From 88d4c55ec38c68c07496ed059616b15622fc83e7 Mon Sep 17 00:00:00 2001 From: uckelman Date: Sun, 31 Oct 2010 20:53:21 +0000 Subject: Major refactoring to make bridge easier to test. git-svn-id: https://vassalengine.svn.sourceforge.net/svnroot/vassalengine/site-src/trunk@7431 67b53d14-2c14-4ace-a08f-0dab2b34000c --- src/Bridge.php | 159 ++------------------ src/BridgeImpl.php | 180 +++++++++++++++++++++++ src/MailmanToPhpBB3.php | 108 ++++++++++++++ src/PhpBB3.php | 340 ++----------------------------------------- src/PhpBB3Impl.php | 364 ++++++++++++++++++++++++++++++++++++++++++++++ src/PhpBB3ToMailman.php | 175 ++++++++++++++++++++++ src/Util.php | 12 +- src/build_email.php | 140 ++++++++++++++++++ src/forum_post_delete.php | 7 +- src/forum_post_send.php | 236 ++---------------------------- src/list_post_receive.php | 90 ++---------- test/UtilTest.php | 48 ++++++ test/build_email_test.php | 150 +++++++++++++++++++ 13 files changed, 1218 insertions(+), 791 deletions(-) create mode 100644 src/BridgeImpl.php create mode 100644 src/MailmanToPhpBB3.php create mode 100644 src/PhpBB3Impl.php create mode 100644 src/PhpBB3ToMailman.php create mode 100644 src/build_email.php create mode 100644 test/UtilTest.php create mode 100644 test/build_email_test.php diff --git a/src/Bridge.php b/src/Bridge.php index dd0a1f1..a84c304 100644 --- a/src/Bridge.php +++ b/src/Bridge.php @@ -20,163 +20,26 @@ # along with this program. If not, see . # -require_once(__DIR__ . '/BridgeConf.php'); -require_once(__DIR__ . '/Util.php'); +interface Bridge { + public function getPostId($messageId); -class Bridge { - protected $db; + public function getMessageId($postId); - public function __construct($db = FALSE) { - $this->db = $db ? $db : - new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS); + public function setPostId($messageId, $postId); - $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } + public function getDefaultForumId($list); - public function getPostId($messageId) { - throw_if_null($messageId); + public function getLists($forumId); - $sql = 'SELECT post_id FROM posts ' . - 'WHERE message_id = ' . $this->db->quote($messageId); + public function reserveEditId($postId); - $row = $this->get_exactly_one_row($sql); - return $row ? $row['post_id'] : false; - } + public function registerByEditId($editId, $messageId, $inReplyTo); - public function getMessageId($postId) { - throw_if_null($postId); + public function registerByMessageId($messageId, $inReplyTo); - $sql = 'SELECT p1.message_id FROM posts AS p1 ' . - 'LEFT OUTER JOIN posts AS p2 ON (' . - 'p1.post_id = p2.post_id AND ' . - 'p1.edit_id < p2.edit_id' . - ') WHERE p1.post_id = ' . $postId . ' AND ' . - 'p2.post_id IS NULL'; + public function unregisterMessage($editId); - $row = $this->get_exactly_one_row($sql); - return $row ? $row['message_id'] : false; - } - - public function setPostId($messageId, $postId) { - throw_if_null($messageId); - throw_if_null($postId); - - $sql = 'UPDATE posts SET ' . - 'post_id = ' . $postId . ' ' . - 'WHERE message_id = ' . $this->db->quote($messageId); - - $count = $this->db->exec($sql); - - if ($count != 1) { - throw new Exception('Failed to set post id: ' . $messageId); - } - } - - public function getDefaultForumId($list) { - throw_if_null($list); - - $sql = 'SELECT forum_id FROM forums ' . - 'WHERE list_name = ' . $this->db->quote($list); - - $row = $this->get_exactly_one_row($sql); - return $row ? $row['forum_id'] : false; - } - - public function getLists($forumId) { - throw_if_null($forumId); - - $sql = 'SELECT list_name FROM lists ' . - 'WHERE forum_id = ' . $forumId; - - $result = $this->db->query($sql); - - $rows = $result->fetchAll(PDO::FETCH_COLUMN); - $result->closeCursor(); - return $rows; - } - - public function reserveEditId($postId) { - throw_if_null($postId); - - $sql = 'INSERT INTO posts (post_id) VALUES (' . $postId . ')'; - - $count = $this->db->exec($sql); - if ($count != 1) { - throw new Exception('Failed to register post id: ' . $postId); - } - - return $this->db->lastInsertId(); - } - - public function registerByEditId($editId, $messageId, $inReplyTo) { - throw_if_null($messageId); - - $sql = 'UPDATE posts SET ' . - 'message_id = ' . $this->db->quote($messageId) . ', ' . - 'in_reply_to = ' . $this->quote($inReplyTo) . ' ' . - 'WHERE edit_id = ' . $editId; - - $count = $this->db->exec($sql); - return $count == 1; - } - - public function registerByMessageId($messageId, $inReplyTo) { - throw_if_null($messageId); - - $sql = 'INSERT IGNORE INTO posts ' . - '(message_id, in_reply_to) ' . - 'VALUES (' . - $this->db->quote($messageId) . ', ' . - $this->quote($inReplyTo) . - ')'; - - $count = $this->db->exec($sql); - return $count == 1 ? $this->db->lastInsertId() : false; - } - - public function unregisterMessage($editId) { - throw_if_null($editId); - - $sql = 'DELETE FROM posts WHERE edit_id = ' . $editId; - - $count = $this->db->exec($sql); - - if ($count != 1) { - throw new Exception('Failed to delete edit id: ' . $editId); - } - } - - public function removePost($postId) { - throw_if_null($postId); - - $sql = 'DELETE FROM posts WHERE post_id = ' . $postId; - - $count = $this->db->exec($sql); - - return $count > 0; - } - - protected function get_exactly_one_row($sql) { - $result = $this->db->query($sql); - - $rows = $result->fetchAll(PDO::FETCH_ASSOC); - $result->closeCursor(); - - switch (count($rows)) { - case 0: - return false; - - case 1: - return $rows[0]; - - default: - throw new Exception("Too many rows returned: $sql"); - } - } - - protected function quote($arg) { - return $arg === null ? 'NULL' : $this->db->quote($arg); - } + public function removePost($postId); } ?> diff --git a/src/BridgeImpl.php b/src/BridgeImpl.php new file mode 100644 index 0000000..df21a89 --- /dev/null +++ b/src/BridgeImpl.php @@ -0,0 +1,180 @@ +. +# + +require_once(__DIR__ . '/Bridge.php'); +require_once(__DIR__ . '/Util.php'); + +class BridgeImpl implements Bridge { + protected $db; + + public function __construct(PDO $db) { + $this->db = $db; + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + public function getPostId($messageId) { + throw_if_null($messageId); + + $sql = 'SELECT post_id FROM posts ' . + 'WHERE message_id = ' . $this->db->quote($messageId); + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['post_id'] : false; + } + + public function getMessageId($postId) { + throw_if_null($postId); + + $sql = 'SELECT p1.message_id FROM posts AS p1 ' . + 'LEFT OUTER JOIN posts AS p2 ON (' . + 'p1.post_id = p2.post_id AND ' . + 'p1.edit_id < p2.edit_id' . + ') WHERE p1.post_id = ' . $postId . ' AND ' . + 'p2.post_id IS NULL'; + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['message_id'] : false; + } + + public function setPostId($messageId, $postId) { + throw_if_null($messageId); + throw_if_null($postId); + + $sql = 'UPDATE posts SET ' . + 'post_id = ' . $postId . ' ' . + 'WHERE message_id = ' . $this->db->quote($messageId); + + $count = $this->db->exec($sql); + + if ($count != 1) { + throw new Exception('Failed to set post id: ' . $messageId); + } + } + + public function getDefaultForumId($list) { + throw_if_null($list); + + $sql = 'SELECT forum_id FROM forums ' . + 'WHERE list_name = ' . $this->db->quote($list); + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['forum_id'] : false; + } + + public function getLists($forumId) { + throw_if_null($forumId); + + $sql = 'SELECT list_name FROM lists ' . + 'WHERE forum_id = ' . $forumId; + + $result = $this->db->query($sql); + + $rows = $result->fetchAll(PDO::FETCH_COLUMN); + $result->closeCursor(); + return $rows; + } + + public function reserveEditId($postId) { + throw_if_null($postId); + + $sql = 'INSERT INTO posts (post_id) VALUES (' . $postId . ')'; + + $count = $this->db->exec($sql); + if ($count != 1) { + throw new Exception('Failed to register post id: ' . $postId); + } + + return $this->db->lastInsertId(); + } + + public function registerByEditId($editId, $messageId, $inReplyTo) { + throw_if_null($messageId); + + $sql = 'UPDATE posts SET ' . + 'message_id = ' . $this->db->quote($messageId) . ', ' . + 'in_reply_to = ' . $this->quote($inReplyTo) . ' ' . + 'WHERE edit_id = ' . $editId; + + $count = $this->db->exec($sql); + return $count == 1; + } + + public function registerByMessageId($messageId, $inReplyTo) { + throw_if_null($messageId); + + $sql = 'INSERT IGNORE INTO posts ' . + '(message_id, in_reply_to) ' . + 'VALUES (' . + $this->db->quote($messageId) . ', ' . + $this->quote($inReplyTo) . + ')'; + + $count = $this->db->exec($sql); + return $count == 1 ? $this->db->lastInsertId() : false; + } + + public function unregisterMessage($editId) { + throw_if_null($editId); + + $sql = 'DELETE FROM posts WHERE edit_id = ' . $editId; + + $count = $this->db->exec($sql); + + if ($count != 1) { + throw new Exception('Failed to delete edit id: ' . $editId); + } + } + + public function removePost($postId) { + throw_if_null($postId); + + $sql = 'DELETE FROM posts WHERE post_id = ' . $postId; + + $count = $this->db->exec($sql); + + return $count > 0; + } + + protected function get_exactly_one_row($sql) { + $result = $this->db->query($sql); + + $rows = $result->fetchAll(PDO::FETCH_ASSOC); + $result->closeCursor(); + + switch (count($rows)) { + case 0: + return false; + + case 1: + return $rows[0]; + + default: + throw new Exception("Too many rows returned: $sql"); + } + } + + protected function quote($arg) { + return $arg === null ? 'NULL' : $this->db->quote($arg); + } +} + +?> diff --git a/src/MailmanToPhpBB3.php b/src/MailmanToPhpBB3.php new file mode 100644 index 0000000..b4ef325 --- /dev/null +++ b/src/MailmanToPhpBB3.php @@ -0,0 +1,108 @@ +. +# + +require_once('Log.php'); + +require_once(__DIR__ . '/Bridge.php'); +require_once(__DIR__ . '/Message.php'); +require_once(__DIR__ . '/PhpBB3.php'); + +class MailmanToPhpBB3 { + protected $bridge; + protected $phpbb; + protected $logger; + + public function __construct(Bridge $bridge, PhpBB3 $phpbb, Log $logger) { + $this->bridge = $bridge; + $this->phpbb = $phpbb; + $this->logger = $logger; + } + + public function process(Message $msg) { + $messageId = $msg->getMessageId(); + $inReplyTo = $msg->getInReplyTo(); + $rererences = $msg->getReferences(); + $soruce = $msg->getSource(); + + $logger->info($messageId . ' received from ' . $source); + + $editId = $this->bridge->registerByMessageId($messageId, $inReplyTo); + + if ($editId === false) { + # This message has already been processed, bail out + $logger->info($messageId . ' already seen, skipping'); + exit; + } + + try { + $forumId = $topicId = null; + $postType = null; + + if ($inReplyTo) { + # Possibly a reply to an existing topic + $parentId = $this->bridge->getPostId($inReplyTo); + if ($parentId === false) { + throw new Exception('unrecognized Reply-To: ' . $inReplyTo); + } + + $ids = $this->phpbb->getTopicAndForumIds($parentId); + if ($ids === false) { + throw new Exception('unrecognized parent id: ' . $parentId); + } + + # Found the parent's forum and topic, post to those + $forumId = $ids['forum_id']; + $topicId = $ids['topic_id']; + $postType = 'reply'; + + $logger->info($messageId . ' replies to ' . $parentId); + } + else { + # A message starting a new topic, post to default forum for its source + $forumId = $this->bridge->getDefaultForumId($source); + if ($forumId === false) { + throw new Exception('unrecognized source: ' . $source); + } + + $postType = 'post'; + + $logger->info($messageId . ' is a new post'); + } + + $logger->info( + $messageId . ' will be posted to ' . $forumId . ':' . $topicId); + + # Post the message to the forum + $postId = $this->phpbb->postMessage($postType, $forumId, $topicId, $msg); + $this->bridge->setPostId($messageId, $postId); + + $logger->info($messageId . ' posted as ' . $postId); + } + catch (Exception $e) { + # Bridging failed, unregister message. + $this->bridge->unregisterMessage($editId); + throw $e; + } + } +} + +?> diff --git a/src/PhpBB3.php b/src/PhpBB3.php index 59f1d43..2f485e2 100644 --- a/src/PhpBB3.php +++ b/src/PhpBB3.php @@ -20,345 +20,25 @@ # along with this program. If not, see . # -require_once(__DIR__ . '/Util.php'); +interface PhpBB3 { + public function getUserId($from); -# phpBB setup -define('IN_PHPBB', true); -require_once(__DIR__ . '/PhpBB3Conf.php'); -$phpEx = substr(strrchr(__FILE__, '.'), 1); -require_once($phpbb_root_path . 'common.' . $phpEx); -require_once($phpbb_root_path . 'includes/functions.' . $phpEx); -require_once($phpbb_root_path . 'includes/functions_posting.' . $phpEx); -require_once($phpbb_root_path . 'includes/functions_user.' . $phpEx); + public function getUserName($id); -class PhpBB3 { - public function __construct() { - } + public function getTopicAndForumIds($post_id); - public function getUserId($from) { - throw_if_null($from); + public function forumExists($forumId); - global $db; + public function topicStatus($topicId); - # NB: There might be multiple user accounts associated with one email - # address. We can only return one user id, so we decide in favor of - # the account which was most recently used to visit the forum. - $sql = 'SELECT u1.user_id FROM ' . USERS_TABLE . ' AS u1 ' . - 'LEFT OUTER JOIN ' . USERS_TABLE . ' AS u2 ON (' . - 'u1.user_email = u2.user_email AND ' . - 'u1.user_lastvisit < u2.user_lastvisit' . - ') WHERE u1.user_email = "' . $db->sql_escape($from) . '" AND ' . - 'u2.user_email IS NULL'; + public function getPostTime($postId); - $row = $this->get_exactly_one_row($sql); - return $row ? $row['user_id'] : false; - } + public function getAttachmentData($attachId); - public function getUserName($id) { - throw_if_null($id); - - # NB: user_get_id_name is pass-by-reference; we copy $id to prevent - # it from being modified, as we might need it for error messages - $ids = array($id); - $err = user_get_id_name($ids, $names); - if ($err) { - throw new Exception("Could not resolve user id $id: $err"); - } - - if (!isset($names[$id])) { - throw new Exception("Unknown user id: $id"); - } - - return $names[$id]; - } - - public function getTopicAndForumIds($post_id) { - throw_if_null($post_id); - - global $db; - - $sql = 'SELECT topic_id, forum_id FROM ' . POSTS_TABLE . ' ' . - 'WHERE post_id = ' . $post_id; - - $row = $this->get_exactly_one_row($sql); - return $row; - } - - public function forumExists($forumId) { - throw_if_null($forumId); - - global $db; - - $sql = 'SELECT 1 FROM ' . FORUMS_TABLE . ' ' . - 'WHERE forum_id = ' . $forumId . ' LIMIT 1'; - - $result = $db->sql_query($sql); - - $rows = $db->sql_fetchrowset($result); - $db->sql_freeresult($result); - - switch (count($rows)) { - case 0: - return false; - - case 1: - return true; - - default: - # Should be impossible due to LIMIT 1. - throw new Exception("Too many rows returned: $sql"); - } - } - - public function topicStatus($topicId) { - throw_if_null($topicId); - - global $db; - - $sql = 'SELECT topic_status FROM ' . TOPICS_TABLE . ' ' . - 'WHERE topic_id = ' . $topicId; - - $row = $this->get_exactly_one_row($sql); - return $row ? $row['topic_status'] : false; - } - - public function getPostTime($postId) { - throw_if_null($postId); - - global $db; - - $sql = 'SELECT post_time FROM ' . POSTS_TABLE . ' ' . - 'WHERE post_id = ' . $postId; - - $row = $this->get_exactly_one_row($sql); - return $row ? $row['post_time'] : false; - } - - public function getAttachmentData($attachId) { - throw_if_null($attachId); - - global $db; - - $sql = 'SELECT physical_filename, real_filename, ' . - 'attach_comment, mimetype ' . - 'FROM ' . ATTACHMENTS_TABLE . ' ' . - 'WHERE attach_id = ' . $attachId; - - $row = $this->get_exactly_one_row($sql); - return $row; - } - - public function postMessage($postType, $forumId, $topicId, $msg) { - throw_if_null($msg); - - if ($postType == 'post') { - # do nothing - } - else if ($postType == 'reply') { - # Check that we're not replying to a locked topic. - $status = $this->topicStatus($topicId); - if ($status === false) { - throw new Exception('topic does not exist: ' . $topicId); - } - - switch ($this->topicStatus($topicId)) { - case ITEM_UNLOCKED: - # normal, ok - break; - case ITEM_LOCKED: - throw new Exception('post to locked topic: ' . $topicId); - break; - case ITEM_MOVED: - # Should not happen, since the only topics with this status - # are new shadow topics created after moves. - throw new Exception('post to moved topic: ' . $topicId); - break; - default: - # Should not happen. - throw new Exception('bad topic status: ' . $topicId); - break; - } - } - else { - # Should not happen. - throw new Exception('bad post type: ' . $postType); - } - - if (!$this->forumExists($forumId)) { - throw new Exception('forum does not exist: ' . $forumId); - } - - $userId = $this->getUserId($msg->getFrom()); - if ($userId === false) { - throw new Exception('unrecognized email address: ' . $msg->getFrom()); - } - - $userName = $this->getUserName($userId); - if ($userName === false) { - throw new Exception('unrecognized user id: ' . $userId); - } - - $subject = $msg->getSubject(); - list($message, $attachments) = $msg->getFlattenedParts(); - -# FIXME: extract the footer pattern into a config file? - # strip the list footer - $message = preg_replace("/^_______________________________________________\nmessages mailing list\nmessages@vassalengine.org\nhttp:\/\/www.vassalengine.org\/mailman\/listinfo\/messages.*/ms", '', $message); - -# TODO: convert > quoting into BBCode - - # handle attachments - $attachment_data = array(); - - foreach ($attachments as $a) { - $attachment_data[] = $this->addAttachment( - $userId, $a['filename'], $a['comment'], $a['mimetype'], $a['data'] - ); - } - - # bring in the PhpBB globals - global $phpEx, $phpbb_root_path, $user, $auth, - $template, $cache, $db, $config; - - # authenticate ourselves - $user->session_create($userId); - $auth->acl($user->data); - -# FIXME: strip list and forum tag from subject - $subject = utf8_normalize_nfc($subject); - $message = utf8_normalize_nfc($message); - - $uid = $bitfield = $options = ''; - - generate_text_for_storage( - $subject, $uid, $bitfield, $options, false, false, false - ); - - generate_text_for_storage( - $message, $uid, $bitfield, $options, true, true, true - ); - - # build the data array for submit_post - $postId = null; - - $data = array( - 'forum_id' => $forumId, - 'topic_id' => &$topicId, - 'post_id' => &$postId, - 'icon_id' => false, - - 'enable_bbcode' => true, - 'enable_smilies' => true, - 'enable_urls' => true, - 'enable_sig' => true, - - 'message' => $message, - 'message_md5' => md5($message), - - 'bbcode_bitfield' => $bitfield, - 'bbcode_uid' => $uid, - - 'post_edit_locked' => 0, - 'topic_title' => $subject, - 'notify_set' => false, - 'notify' => false, - 'post_time' => 0, - 'forum_name' => '', - 'enable_indexing' => true - ); - - if (!empty($attachment_data)) { - $data['attachment_data'] = $attachment_data; - } - - $poll = ''; - - submit_post($postType, $subject, $userName, POST_NORMAL, $poll, $data); - - return $postId; - } + public function postMessage($postType, $forumId, $topicId, $msg); public function addAttachment($userId, $filename, $comment, - $mimetype, $data) { - throw_if_null($userId); - throw_if_null($filename); - throw_if_null($mimetype); - throw_if_null($data); - - global $db; - -# TODO: check that attachment is a permissible type, size - - # lifted from include/functions_upload.php: filespec::clean_filename() - $physicalFilename = $userId . '_' . md5(unique_id()); - - # get extension - $dot = strrpos($filename, '.'); - $extension = $dot === false ? '' : substr($filename, $dot + 1); - - # put the attachment data into the db - $sql = 'INSERT INTO ' . ATTACHMENTS_TABLE . ' (' . - 'poster_id, is_orphan, physical_filename, real_filename, ' . - 'attach_comment, extension, mimetype, filesize, filetime' . - ') VALUES (' . - $userId . ', ' . - '1, ' . - '"' . $physicalFilename . '", ' . - '"' . $db->sql_escape($filename) . '", ' . - '"' . $db->sql_escape($comment) . '", ' . - '"' . $db->sql_escape($extension) . '", ' . - '"' . $db->sql_escape($mimetype) . '", ' . - strlen($data) . ', ' . - time() . - ')'; - - $db->sql_query($sql); - - if ($db->sql_affectedrows() != 1) { - throw new Exception("Adding attachment failed: $sql"); - } - - # post the attachment data to our attachment writer shim - require_once(__DIR__ . '/HTTP_POST_multipart.php'); - - $url = 'http://www.test.nomic.net/forum/attachment_writer.php'; - $poster = new HTTP_POST_multipart(); - $poster->addData('password', '5rnudbp7dLkijcwrT@sz'); - $poster->addFile(1, $physicalFilename, $mimetype, null, 'binary', $data); - $result = $poster->post($url); - - if ($result != 1) { - throw new Exception('Attachment writer failed: ' . $result); - } - - # return the attachment info needed by submit_post - return array( - 'attach_id' => $db->sql_nextid(), - 'is_orphan' => 1, - 'real_filename' => $realFilename, - 'attach_comment' => $comment, - ); - } - - protected function get_exactly_one_row($sql) { - global $db; - - $result = $db->sql_query($sql); - - $rows = $db->sql_fetchrowset($result); - $db->sql_freeresult($result); - - switch (count($rows)) { - case 0: - return false; - - case 1: - return $rows[0]; - - default: - throw new Exception("Too many rows returned: $sql"); - } - } + $mimetype, $data); } ?> diff --git a/src/PhpBB3Impl.php b/src/PhpBB3Impl.php new file mode 100644 index 0000000..9986a6a --- /dev/null +++ b/src/PhpBB3Impl.php @@ -0,0 +1,364 @@ +. +# + +require_once(__DIR__ . '/PhpBB3.php'); +require_once(__DIR__ . '/Util.php'); + +# phpBB setup +define('IN_PHPBB', true); +$phpEx = substr(strrchr(__FILE__, '.'), 1); +require_once($phpbb_root_path . 'common.' . $phpEx); +require_once($phpbb_root_path . 'includes/functions.' . $phpEx); +require_once($phpbb_root_path . 'includes/functions_posting.' . $phpEx); +require_once($phpbb_root_path . 'includes/functions_user.' . $phpEx); + +class PhpBB3Impl implements PhpBB3 { + public function __construct() { + } + + public function getUserId($from) { + throw_if_null($from); + + global $db; + + # NB: There might be multiple user accounts associated with one email + # address. We can only return one user id, so we decide in favor of + # the account which was most recently used to visit the forum. + $sql = 'SELECT u1.user_id FROM ' . USERS_TABLE . ' AS u1 ' . + 'LEFT OUTER JOIN ' . USERS_TABLE . ' AS u2 ON (' . + 'u1.user_email = u2.user_email AND ' . + 'u1.user_lastvisit < u2.user_lastvisit' . + ') WHERE u1.user_email = "' . $db->sql_escape($from) . '" AND ' . + 'u2.user_email IS NULL'; + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['user_id'] : false; + } + + public function getUserName($id) { + throw_if_null($id); + + # NB: user_get_id_name is pass-by-reference; we copy $id to prevent + # it from being modified, as we might need it for error messages + $ids = array($id); + $err = user_get_id_name($ids, $names); + if ($err) { + throw new Exception("Could not resolve user id $id: $err"); + } + + if (!isset($names[$id])) { + throw new Exception("Unknown user id: $id"); + } + + return $names[$id]; + } + + public function getTopicAndForumIds($post_id) { + throw_if_null($post_id); + + global $db; + + $sql = 'SELECT topic_id, forum_id FROM ' . POSTS_TABLE . ' ' . + 'WHERE post_id = ' . $post_id; + + $row = $this->get_exactly_one_row($sql); + return $row; + } + + public function forumExists($forumId) { + throw_if_null($forumId); + + global $db; + + $sql = 'SELECT 1 FROM ' . FORUMS_TABLE . ' ' . + 'WHERE forum_id = ' . $forumId . ' LIMIT 1'; + + $result = $db->sql_query($sql); + + $rows = $db->sql_fetchrowset($result); + $db->sql_freeresult($result); + + switch (count($rows)) { + case 0: + return false; + + case 1: + return true; + + default: + # Should be impossible due to LIMIT 1. + throw new Exception("Too many rows returned: $sql"); + } + } + + public function topicStatus($topicId) { + throw_if_null($topicId); + + global $db; + + $sql = 'SELECT topic_status FROM ' . TOPICS_TABLE . ' ' . + 'WHERE topic_id = ' . $topicId; + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['topic_status'] : false; + } + + public function getPostTime($postId) { + throw_if_null($postId); + + global $db; + + $sql = 'SELECT post_time FROM ' . POSTS_TABLE . ' ' . + 'WHERE post_id = ' . $postId; + + $row = $this->get_exactly_one_row($sql); + return $row ? $row['post_time'] : false; + } + + public function getAttachmentData($attachId) { + throw_if_null($attachId); + + global $db; + + $sql = 'SELECT physical_filename, real_filename, ' . + 'attach_comment, mimetype ' . + 'FROM ' . ATTACHMENTS_TABLE . ' ' . + 'WHERE attach_id = ' . $attachId; + + $row = $this->get_exactly_one_row($sql); + return $row; + } + + public function postMessage($postType, $forumId, $topicId, $msg) { + throw_if_null($msg); + + if ($postType == 'post') { + # do nothing + } + else if ($postType == 'reply') { + # Check that we're not replying to a locked topic. + $status = $this->topicStatus($topicId); + if ($status === false) { + throw new Exception('topic does not exist: ' . $topicId); + } + + switch ($this->topicStatus($topicId)) { + case ITEM_UNLOCKED: + # normal, ok + break; + case ITEM_LOCKED: + throw new Exception('post to locked topic: ' . $topicId); + break; + case ITEM_MOVED: + # Should not happen, since the only topics with this status + # are new shadow topics created after moves. + throw new Exception('post to moved topic: ' . $topicId); + break; + default: + # Should not happen. + throw new Exception('bad topic status: ' . $topicId); + break; + } + } + else { + # Should not happen. + throw new Exception('bad post type: ' . $postType); + } + + if (!$this->forumExists($forumId)) { + throw new Exception('forum does not exist: ' . $forumId); + } + + $userId = $this->getUserId($msg->getFrom()); + if ($userId === false) { + throw new Exception('unrecognized email address: ' . $msg->getFrom()); + } + + $userName = $this->getUserName($userId); + if ($userName === false) { + throw new Exception('unrecognized user id: ' . $userId); + } + + $subject = $msg->getSubject(); + list($message, $attachments) = $msg->getFlattenedParts(); + +# FIXME: extract the footer pattern into a config file? + # strip the list footer + $message = preg_replace("/^_______________________________________________\nmessages mailing list\nmessages@vassalengine.org\nhttp:\/\/www.vassalengine.org\/mailman\/listinfo\/messages.*/ms", '', $message); + +# TODO: convert > quoting into BBCode + + # handle attachments + $attachment_data = array(); + + foreach ($attachments as $a) { + $attachment_data[] = $this->addAttachment( + $userId, $a['filename'], $a['comment'], $a['mimetype'], $a['data'] + ); + } + + # bring in the PhpBB globals + global $phpEx, $phpbb_root_path, $user, $auth, + $template, $cache, $db, $config; + + # authenticate ourselves + $user->session_create($userId); + $auth->acl($user->data); + +# FIXME: strip list and forum tag from subject + $subject = utf8_normalize_nfc($subject); + $message = utf8_normalize_nfc($message); + + $uid = $bitfield = $options = ''; + + generate_text_for_storage( + $subject, $uid, $bitfield, $options, false, false, false + ); + + generate_text_for_storage( + $message, $uid, $bitfield, $options, true, true, true + ); + + # build the data array for submit_post + $postId = null; + + $data = array( + 'forum_id' => $forumId, + 'topic_id' => &$topicId, + 'post_id' => &$postId, + 'icon_id' => false, + + 'enable_bbcode' => true, + 'enable_smilies' => true, + 'enable_urls' => true, + 'enable_sig' => true, + + 'message' => $message, + 'message_md5' => md5($message), + + 'bbcode_bitfield' => $bitfield, + 'bbcode_uid' => $uid, + + 'post_edit_locked' => 0, + 'topic_title' => $subject, + 'notify_set' => false, + 'notify' => false, + 'post_time' => 0, + 'forum_name' => '', + 'enable_indexing' => true + ); + + if (!empty($attachment_data)) { + $data['attachment_data'] = $attachment_data; + } + + $poll = ''; + + submit_post($postType, $subject, $userName, POST_NORMAL, $poll, $data); + + return $postId; + } + + public function addAttachment($userId, $filename, $comment, + $mimetype, $data) { + throw_if_null($userId); + throw_if_null($filename); + throw_if_null($mimetype); + throw_if_null($data); + + global $db; + +# TODO: check that attachment is a permissible type, size + + # lifted from include/functions_upload.php: filespec::clean_filename() + $physicalFilename = $userId . '_' . md5(unique_id()); + + # get extension + $dot = strrpos($filename, '.'); + $extension = $dot === false ? '' : substr($filename, $dot + 1); + + # put the attachment data into the db + $sql = 'INSERT INTO ' . ATTACHMENTS_TABLE . ' (' . + 'poster_id, is_orphan, physical_filename, real_filename, ' . + 'attach_comment, extension, mimetype, filesize, filetime' . + ') VALUES (' . + $userId . ', ' . + '1, ' . + '"' . $physicalFilename . '", ' . + '"' . $db->sql_escape($filename) . '", ' . + '"' . $db->sql_escape($comment) . '", ' . + '"' . $db->sql_escape($extension) . '", ' . + '"' . $db->sql_escape($mimetype) . '", ' . + strlen($data) . ', ' . + time() . + ')'; + + $db->sql_query($sql); + + if ($db->sql_affectedrows() != 1) { + throw new Exception("Adding attachment failed: $sql"); + } + + # post the attachment data to our attachment writer shim + require_once(__DIR__ . '/HTTP_POST_multipart.php'); + + $url = 'http://www.test.nomic.net/forum/attachment_writer.php'; + $poster = new HTTP_POST_multipart(); + $poster->addData('password', '5rnudbp7dLkijcwrT@sz'); + $poster->addFile(1, $physicalFilename, $mimetype, null, 'binary', $data); + $result = $poster->post($url); + + if ($result != 1) { + throw new Exception('Attachment writer failed: ' . $result); + } + + # return the attachment info needed by submit_post + return array( + 'attach_id' => $db->sql_nextid(), + 'is_orphan' => 1, + 'real_filename' => $realFilename, + 'attach_comment' => $comment, + ); + } + + protected function get_exactly_one_row($sql) { + global $db; + + $result = $db->sql_query($sql); + + $rows = $db->sql_fetchrowset($result); + $db->sql_freeresult($result); + + switch (count($rows)) { + case 0: + return false; + + case 1: + return $rows[0]; + + default: + throw new Exception("Too many rows returned: $sql"); + } + } +} + +?> diff --git a/src/PhpBB3ToMailman.php b/src/PhpBB3ToMailman.php new file mode 100644 index 0000000..fccd2b4 --- /dev/null +++ b/src/PhpBB3ToMailman.php @@ -0,0 +1,175 @@ +. +# + +require_once('Log.php'); +require_once('Mail.php'); + +require_once(__DIR__ . '/BBCodeParser.php'); +require_once(__DIR__ . '/Bridge.php'); +require_once(__DIR__ . '/PhpBB3.php'); +require_once(__DIR__ . '/Util.php'); +require_once(__DIR__ . '/build_email.php'); + +class PhpBB3ToMailman { + + protected $bridge; + protected $phpbb; + protected $logger; + + public function __construct(Bridge $bridge, PhpBB3 $phpbb, Log $logger) { + $this->bridge = $bridge; + $this->phpbb = $phpbb; + $this->logger = $logger; + } + + public function process($config, $user, $mode, $data, $post_data) { + # Sanity check + if (!in_array($mode, array('post', 'reply', 'quote', 'edit'))) { + throw new Exception('unrecognized mode: ' . $mode); + } + + $postId = $data['post_id']; + $forumId = $data['forum_id']; + + $this->logger->info($postId . ' received from phpBB forum ' . $forumId); + + $to = $this->bridge->getLists($forumId); + if (count($to) == 0) { + # No lists to send to, bail out. + return; + } + $to = implode(', ', $to); + + $userName = $user->data['username']; + $userEmail = $user->data['user_email']; + + $sender = 'forum-bridge@vassalengine.org'; + + $subject = html_entity_decode( + '[' . $post_data['forum_name'] . '] ' . $post_data['post_subject'], + ENT_QUOTES + ); + + $time = null; + if ($mode == 'edit') { + # Post time is NOT updated on edit, so we get the current time + $time = time(); + } + else { + $time = $this->phpbb->getPostTime($postId); + if ($time === false) { + throw new Exception('no post time: ' . $postId); + } + } + + $inReplyTo = null; + $references = null; + + if ($mode == 'reply' || $mode == 'quote') { + $firstId = $data['topic_first_post_id']; + $firstMessageId = $this->bridge->getMessageId($firstId); + if ($firstMessageId === false) { + $this->logger->info($postId . ' replies to an unknown message'); + } + else { + $inReplyTo = $references = $firstMessageId; + $this->logger->info($postId . ' replies to ' . $firstMessageId); + } + } + else if ($mode == 'edit') { + $inReplyTo = $this->bridge->getMessageId($postId); + } + + $forumURL = 'http://' . $_SERVER['SERVER_NAME'] . + dirname($_SERVER['SCRIPT_NAME']); + + $editId = $this->bridge->reserveEditId($postId); + $messageId = build_message_id($postId, $editId, + $time, $_SERVER['SERVER_NAME']); + + # Assemble the message headers + $headers = build_headers( + $userName, + $userEmail, + $to, + $sender, + $subject, + $mode == 'edit', + $time, + $messageId, + $forumURL, + $inReplyTo, + $references + ); + + # Build the message body + $parser = new BBCodeParser(); + $text = $parser->parse($data['message'], $data['bbcode_uid']); + $text = build_text($text, $mode == 'edit'); + + # Build the bridge footer + $footer = build_footer($postId, $forumURL); + + $attachments = array(); + foreach ($data['attachment_data'] as $a) { + $attachId = $a['attach_id']; + + $adata = $this->phpbb->getAttachmentData($attachId); + if ($adata === false) { + throw new Exception('unrecognized attachment id: ' . $attachId); + } + + $adata['path'] = $phpbb_root_path . $config['upload_path'] . '/' . + utf8_basename($adata['physical_filename']); + + $attachments[] = $adata; + } + + # Build the message body + $body = build_body($headers, $text, $attachments, $footer); + + $mailer = Mail::factory('sendmail'); + + # Register the message + $seen = !$this->bridge->registerByEditId($editId, $messageId, $inReplyTo); + if ($seen) { + throw new Exception('message id already seen: ' . $messageId); + } + + try { + # Send the message + $err = $mailer->send($to, $headers, $body); + if (PEAR::isError($err)) { + throw new Exception('Mail::send error: ' . $err->toString()); + } + + $this->logger->info($postId . ' sent to ' . $to . ' as ' . $messageId); + } + catch (Exception $e) { + # Bridging failed, unregister message. + $this->bridge->unregisterMessage($editId); + throw $e; + } + } +} + +?> diff --git a/src/Util.php b/src/Util.php index 2535bc5..0f1499a 100644 --- a/src/Util.php +++ b/src/Util.php @@ -24,16 +24,16 @@ function throw_if_null($arg) { if ($arg === null) throw new Exception('argument is null'); } -function build_message_id($postId, $editId, $time, $forumHost) { - return "<$time.$postId.$editId.bridge@$forumHost>"; +function is_ascii($str) { + return !preg_match('/[^[:ascii:]]/', $str); } -function is_ascii($string) { - return !preg_match('/[^[:ascii:]]/', $str); +function utf8_quote($str) { + return '=?UTF-8?B?' . base64_encode($str) . '?='; } -function utf8_quote($string) { - return '=?UTF-8?B?' . base64_encode($string) . '?='; +function utf8_quote_non_ascii($str) { + return is_ascii($str) ? $str : utf8_quote($str); } ?> diff --git a/src/build_email.php b/src/build_email.php new file mode 100644 index 0000000..5301106 --- /dev/null +++ b/src/build_email.php @@ -0,0 +1,140 @@ + 'multipart/mixed'); + $mime = new Mail_mimePart('', $params); + + # Build the main body + build_text_part($mime, $text); + + # Build each attachment + foreach ($attachments as $a) { + $bytes = file_get_contents($a['path']); + if ($bytes === false) { + throw new Exception('failed to read file: ' . $a['path']); + } + + build_attachment( + $mime, + $adata['mimetype'], + $adata['real_filename'], + $adata['attach_comment'], + $bytes + ); + } + + # Build footer + build_text_part($mime, $footer); + + # Encode the message + $msg = $mime->encode(); + $headers = array_merge($headers, $msg['headers']); + $body = $msg['body']; + } + + return $body; +} + +function build_headers($userName, $userEmail, $to, $sender, $subject, $edit, + $time, $messageId, $forumURL, $inReplyTo, $references) { + + $from = sprintf('%s <%s>', utf8_quote_non_ascii($userName), $userEmail); + $subject = utf8_quote_non_ascii($subject); + $date = date(DATE_RFC2822, $time); + + if ($edit) { + $edit_header = 'Edit: '; + $subject = $edit_header . $subject; + } + + $headers = array( + 'To' => $to, + 'From' => $from, + 'Sender' => $sender, + 'Subject' => $subject, + 'Date' => $date, + 'Message-ID' => $messageId, + 'X-BeenThere' => $forumURL + ); + + if ($inReplyTo !== null) { + $headers['In-Reply-To'] = $inReplyTo; + } + + if ($references !== null) { + $headers['References'] = $references; + } + + return $headers; +} + +function build_text_part(Mail_mimePart $mime, $text) { + $params = array( + 'content_type' => 'text/plain', + 'charset' => 'utf-8', + 'encoding' => '8bit', + 'disposition' => 'inline' + ); + $mime->addSubPart($text, $params); +} + +function build_attachment(Mail_mimePart $mime, $type, + $filename, $descr, $data) { + $params = array( + 'content_type' => $type, + 'encoding' => 'base64', + 'disposition' => 'attachment', + 'dfilename' => $filename, + 'description' => $descr + ); + $mime->addSubPart($data, $params); +} + +function build_message_id($postId, $editId, $time, $forumHost) { + return "<$time.$postId.$editId.bridge@$forumHost>"; +} + +?> diff --git a/src/forum_post_delete.php b/src/forum_post_delete.php index f80b1ad..be3fa0f 100644 --- a/src/forum_post_delete.php +++ b/src/forum_post_delete.php @@ -38,9 +38,12 @@ function remove_post($postId) { require_once('Log.php'); $logger = &Log::singleton('file', '/var/log/listbridge', 'one'); - require_once(__DIR__ . '/Bridge.php'); + require_once(__DIR__ . '/BridgeConf.php') + require_once(__DIR__ . '/BridgeImpl.php'); + + $db = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS); + $bridge = new BridgeImpl($db); - $bridge = new Bridge(); if ($bridge->removePost($postId)) { $logger->info($postId . ' deleted'); } diff --git a/src/forum_post_send.php b/src/forum_post_send.php index ffab780..c2b23bd 100644 --- a/src/forum_post_send.php +++ b/src/forum_post_send.php @@ -34,7 +34,6 @@ catch (Exception $e) { } function send_post_to_lists($config, $user, $mode, $data, $post_data) { - require_once('Log.php'); $logger = &Log::singleton('file', '/var/log/listbridge', 'one'); @@ -45,234 +44,19 @@ function send_post_to_lists($config, $user, $mode, $data, $post_data) { print '

'; */ - # Sanity check - if (!in_array($mode, array('post', 'reply', 'quote', 'edit'))) { - throw new Exception('unrecognized mode: ' . $mode); - } - - require_once('Mail.php'); - - require_once(__DIR__ . '/BBCodeParser.php'); - require_once(__DIR__ . '/Bridge.php'); - require_once(__DIR__ . '/PhpBB3.php'); - require_once(__DIR__ . '/Util.php'); - - $postId = $data['post_id']; - $forumId = $data['forum_id']; + require_once(__DIR__ . '/BridgeConf.php'); + require_once(__DIR__ . '/BridgeImpl.php'); + require_once(__DIR__ . '/PhpBB3Conf.php'); + require_once(__DIR__ . '/PhpBB3Impl.php'); + require_once(__DIR__ . '/PhpBB3ToMailman.php'); - $logger->info($postId . ' received from phpBB forum ' . $forumId); - - $bridge = new Bridge(); - - $to = $bridge->getLists($forumId); - if (count($to) == 0) { - # No lists to send to, bail out. - return; - } - $to = implode(', ', $to); - - $userName = $user->data['username']; - $userEmail = $user->data['user_email']; - - # NB: Don't use utf8_quote on things which don't need it. - $from = (is_ascii($userName) ? $userName : utf8_quote($userName)) . - ' <' . $userEmail . '>'; - - $sender = 'forum-bridge@vassalengine.org'; - - $subject = html_entity_decode( - '[' . $post_data['forum_name'] . '] ' . $post_data['post_subject'], - ENT_QUOTES - ); - - if (!is_ascii($subject)) { - $subject = utf8_quote($subject); - } - - $phpbb = new PhpBB3(); - - $time = null; - if ($mode == 'edit') { - # Post time is NOT updated on edit, so we get the current time - $time = time(); - } - else { - $time = $phpbb->getPostTime($postId); - if ($time === false) { - throw new Exception('no post time: ' . $postId); - } - } - - $date = date(DATE_RFC2822, $time); - - $inReplyTo = null; - $references = null; - - if ($mode == 'reply' || $mode == 'quote') { - $firstId = $data['topic_first_post_id']; - $firstMessageId = $bridge->getMessageId($firstId); - if ($firstMessageId === false) { - $logger->info($postId . ' replies to an unknown message'); - } - else { - $inReplyTo = $references = $firstMessageId; - $logger->info($postId . ' replies to ' . $firstMessageId); - } - } - else if ($mode == 'edit') { - $inReplyTo = $bridge->getMessageId($postId); - } - - $forumURL = 'http://' . $_SERVER['SERVER_NAME'] . - dirname($_SERVER['SCRIPT_NAME']); - - $editId = $bridge->reserveEditId($postId); - $messageId = build_message_id($postId, $editId, - $time, $_SERVER['SERVER_NAME']); - - # Assemble the message headers - $headers = array( - 'To' => $to, - 'From' => $from, - 'Sender' => $sender, - 'Subject' => $subject, - 'Date' => $date, - 'Message-ID' => $messageId, - 'X-BeenThere' => $forumURL - ); - - if ($inReplyTo !== null) { - $headers['In-Reply-To'] = $inReplyTo; - } - - if ($references !== null) { - $headers['References'] = $references; - } - - # Build the message body - $parser = new BBCodeParser(); - $text = $parser->parse($data['message'], $data['bbcode_uid']); - - if ($mode == 'edit') { - $edit_notice = << 'multipart/mixed'); - $mime = new Mail_mimePart('', $params); - - # Build the main body - build_text_part($mime, $text); - - # Build each attachment - foreach ($data['attachment_data'] as $a) { - $attachId = $a['attach_id']; - $adata = $phpbb->getAttachmentData($attachId); - if ($adata === false) { - throw new Exception('unrecognized attachment id: ' . $attachId); - } - - $afile = $phpbb_root_path . $config['upload_path'] . '/' . - utf8_basename($adata['physical_filename']); - - $bytes = file_get_contents($afile); - if ($bytes === false) { - throw new Exception('failed to read file: ' . $afile); - } - - build_attachment( - $mime, - $adata['mimetype'], - $adata['real_filename'], - $adata['attach_comment'], - $bytes - ); - } - - # Build footer - build_text_part($mime, $footer); - - # Encode the message - $msg = $mime->encode(); - $headers = array_merge($headers, $msg['headers']); - $body = $msg['body']; - } - - $mailer = Mail::factory('sendmail'); - - # Register the message - $seen = !$bridge->registerByEditId($editId, $messageId, $inReplyTo); - if ($seen) { - throw new Exception('message id already seen: ' . $messageId); - } - - try { - # Send the message - $err = $mailer->send($to, $headers, $body); - if (PEAR::isError($err)) { - throw new Exception('Mail::send error: ' . $err->toString()); - } - - $logger->info($postId . ' sent to ' . $to . ' as ' . $messageId); - } - catch (Exception $e) { - # Bridging failed, unregister message. - $bridge->unregisterMessage($editId); - throw $e; - } -} - -function build_text_part($mime, $text) { - $params = array( - 'content_type' => 'text/plain', - 'charset' => 'utf-8', - 'encoding' => '8bit', - 'disposition' => 'inline' - ); - $mime->addSubPart($text, $params); -} + $phpbb = new PhpBB3Impl(); -function build_attachment($mime, $type, $filename, $descr, $data) { - $params = array( - 'content_type' => $type, - 'encoding' => 'base64', - 'disposition' => 'attachment', - 'dfilename' => $filename, - 'description' => $descr - ); - $mime->addSubPart($data, $params); + $conduit = new PhpBB3ToMailman($bridge, $phpbb, $logger); + $conduit->process($config, $user, $mode, $data, $post_data); } ?> diff --git a/src/list_post_receive.php b/src/list_post_receive.php index 2072742..2c5ddc2 100644 --- a/src/list_post_receive.php +++ b/src/list_post_receive.php @@ -20,97 +20,29 @@ # along with this program. If not, see . # -# TODO: logging! -# TODO: Refactor postMessage(). - require_once('Log.php'); $logger = &Log::singleton('file', '/var/log/listbridge', 'one'); try { - require_once('/var/www/bridge/src/Bridge.php'); - require_once('/var/www/bridge/src/MailmanLib.php'); - require_once('/var/www/bridge/src/MailmanMessage.php'); - require_once('/var/www/bridge/src/PhpBB3.php'); - - # Read the message from STDIN -# $url = 'php://stdin'; - -# $input = read_raw_message($url); -# $msg = new MailmanMessage($input); - if (!isset($_POST['message'])) { throw new Exception('No message in POST'); } - $msg = new MailmanMessage($_POST['message']); - - $messageId = $msg->getMessageId(); - $inReplyTo = $msg->getInReplyTo(); - $rererences = $msg->getReferences(); - $soruce = $msg->getSource(); - - $logger->info($messageId . ' received from ' . $source); - - $bridge = new Bridge(); - $editId = $bridge->registerByMessageId($messageId, $inReplyTo); - - if ($editId === false) { - # This message has already been processed, bail out - $logger->info($messageId . ' already seen, skipping'); - exit; - } - - try { - $phpbb = new PhpBB3(); - - $forumId = $topicId = null; - $postType = null; - - if ($inReplyTo) { - # Possibly a reply to an existing topic - $parentId = $bridge->getPostId($inReplyTo); - if ($parentId === false) { - throw new Exception('unrecognized Reply-To: ' . $inReplyTo); - } + require_once(__DIR__ . '/BridgeConf.php'); + require_once(__DIR__ . '/BridgeImpl.php'); + require_once(__DIR__ . '/PhpBB3Conf.php'); + require_once(__DIR__ . '/PhpBB3Impl.php'); + require_once(__DIR__ . '/MailmanToPhpBB3.php'); - $ids = $phpbb->getTopicAndForumIds($parentId); - if ($ids === false) { - throw new Exception('unrecognized parent id: ' . $parentId); - } - - # Found the parent's forum and topic, post to those - $forumId = $ids['forum_id']; - $topicId = $ids['topic_id']; - $postType = 'reply'; - - $logger->info($messageId . ' replies to ' . $parentId); - } - else { - # A message starting a new topic, post to default forum for its source - $forumId = $bridge->getDefaultForumId($source); - if ($forumId === false) { - throw new Exception('unrecognized source: ' . $source); - } - - $postType = 'post'; + $msg = new MailmanMessage($_POST['message']); - $logger->info($messageId . ' is a new post'); - } + $db = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB, DB_USER, DB_PASS); + $bridge = new BridgeImpl($db); - $logger->info( - $messageId . ' will be posted to ' . $forumId . ':' . $topicId); - - # Post the message to the forum - $postId = $phpbb->postMessage($postType, $forumId, $topicId, $msg); - $bridge->setPostId($messageId, $postId); + $phpbb = new PhpBB3Impl(); - $logger->info($messageId . ' posted as ' . $postId); - } - catch (Exception $e) { - # Bridging failed, unregister message. - $bridge->unregisterMessage($editId); - throw $e; - } + $conduit = new MailmanToPhpBB3($bridge, $phpbb, $logger); + $conduit->process($msg); } catch (Exception $e) { $logger->err($e); diff --git a/test/UtilTest.php b/test/UtilTest.php new file mode 100644 index 0000000..7135b9f --- /dev/null +++ b/test/UtilTest.php @@ -0,0 +1,48 @@ +assertEquals($expected, is_ascii($string)); + } + + public function utf8_quote_provider() { + return array( + array('', '=?UTF-8?B??='), + array('foo', '=?UTF-8?B?Zm9v?='), + array('Heizölrückstoßabdämpfung', '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?=') + ); + } + + /** @dataProvider utf8_quote_provider */ + public function test_utf8_quote($string, $expected) { + $this->assertEquals($expected, utf8_quote($string)); + } + + public function utf8_quote_non_ascii_provider() { + return array( + array('', ''), + array('foo', 'foo'), + array('Heizölrückstoßabdämpfung', '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?=') + ); + } + + /** @dataProvider utf8_quote_non_ascii_provider */ + public function test_utf8_quote_non_ascii($string, $expected) { + $this->assertEquals($expected, utf8_quote_non_ascii($string)); + } +} + +?> diff --git a/test/build_email_test.php b/test/build_email_test.php new file mode 100644 index 0000000..c78ab7b --- /dev/null +++ b/test/build_email_test.php @@ -0,0 +1,150 @@ +assertEquals( + "[This message has been edited.] + +foo bar", + build_text('foo bar', true) + ); + } + + public function test_build_text_no_edit() { + $this->assertEquals( + 'foo bar', + build_text('foo bar', false) + ); + } + + public function test_build_footer() { + $this->assertEquals( + " +_______________________________________________ +Read this topic online here: +http://www.example.com/viewtopic.php?p=42#p42", + build_footer(42, 'http://www.example.com') + ); + } + + protected $default_headers = array( + 'To' => 'messages@vassalengine.org', + 'From' => 'Joel Uckelman ', + 'Sender' => 'forum-bridge@vassalengine.org', + 'Subject' => 'Test message', + 'Date' => 'Sun, 31 Oct 2010 08:46:00 -0700', + 'Message-ID' => '<20100302094228.33F0310091@charybdis.ellipsis.cx>', + 'X-BeenThere' => 'http://www.example.com', + 'In-Reply-To' => '<1267473003.m2f.17543@www.vassalengine.org>', + 'References' => '<1267171317.m2f.17507@www.vassalengine.org> <1267473003.m2f.17543@www.vassalengine.org>' + ); + + protected $default_headers_params = array( + 'Joel Uckelman', + 'uckelman@nomic.net', + 'messages@vassalengine.org', + 'forum-bridge@vassalengine.org', + 'Test message', + false, + 1288539960, + '<20100302094228.33F0310091@charybdis.ellipsis.cx>', + 'http://www.example.com', + '<1267473003.m2f.17543@www.vassalengine.org>', + '<1267171317.m2f.17507@www.vassalengine.org> <1267473003.m2f.17543@www.vassalengine.org>' + ); + + protected function call_build_headers(array $headers, array $params) { + $this->assertEquals( + $headers, + build_headers( + $params[0], + $params[1], + $params[2], + $params[3], + $params[4], + $params[5], + $params[6], + $params[7], + $params[8], + $params[9], + $params[10] + ) + ); + } + + public function test_build_headers() { + $headers = $this->default_headers; + $headers_params = $this->default_headers_params; + $this->call_build_headers($headers, $headers_params); + } + + public function test_build_headers_no_in_reply_to() { + $headers = $this->default_headers; + $headers_params = $this->default_headers_params; + + unset($headers['In-Reply-To']); + $headers_params[9] = null; + + $this->call_build_headers($headers, $headers_params); + } + + public function test_build_headers_no_references() { + $headers = $this->default_headers; + $headers_params = $this->default_headers_params; + + unset($headers['References']); + $headers_params[10] = null; + + $this->call_build_headers($headers, $headers_params); + } + + public function test_build_headers_utf8_subject() { + $headers = $this->default_headers; + $headers_params = $this->default_headers_params; + + $headers['Subject'] = '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?='; + $headers_params[4] = 'Heizölrückstoßabdämpfung'; + + $this->call_build_headers($headers, $headers_params); + } + + public function test_build_headers_utf8_username() { + $headers = $this->default_headers; + $headers_params = $this->default_headers_params; + + $headers['From'] = '=?UTF-8?B?SGVpesO2bHLDvGNrc3Rvw59hYmTDpG1wZnVuZw==?= '; + $headers_params[0] = 'Heizölrückstoßabdämpfung'; + + $this->call_build_headers($headers, $headers_params); + } + + public function test_build_body_no_attachments() { + $headers = array(); + $text = 'This is some test text.'; + $footer = " +_______________________________________________ +Read this topic online here: +http://www.example.com/viewtopic.php?p=42#p42"; + $attachments = null; + + $body = build_body($headers, $text, $attachments, $footer); + + + $this->assertEquals('text/plain; charset=UTF-8; format=flowed', $headers['Content-Type']); + $this->assertEquals('8bit', $headers['Content-Transfer-Encoding']); + $this->assertEquals("$text\n$footer", $body); + } + + public function test_build_body_attachments() { + // FIXME: This is kind of a complex test to write, because the result + // is a Mail_mimePart object. + $this->markTestIncomplete(); + } +} + +?> -- cgit v1.2.1