papercut-0.9.13.orig/0000755000175000017500000000000010327536617013424 5ustar lunarlunarpapercut-0.9.13.orig/INSTALL0000644000175000017500000000662410327536616014464 0ustar lunarlunar-------------------- Papercut NNTP Server -------------------- For more information on what exactly Papercut is, and what are its main objectives / intended audience, please read the README file. Requirements: ------------- - Python 2.2 (it needs the email module to parse MIME based messages) - Database server - The Phorum backend needs MySQL or PostgreSQL as the storage driver) - MySQLdb python module (http://sf.net/projects/mysql-python) or - pyPgSQL python module (http://pypgsql.sourceforge.net/) - The phpBB backend needs MySQL - MySQLdb python module (http://sf.net/projects/mysql-python) - There are other storage modules, seek the source for details on those - Permission to add a new column to one of the Phorum tables Step by Step Instructions: -------------------------- 1) Unpack the distribution tarball where you intend to run it from $ cd /path/to/where/papercut/will/run $ gunzip papercut-X.tar.gz $ tar vxf papercut-X.tar [lots of stuff shows up on the console] $ cd papercut-X 2) Edit the settings.py configuration file - Change the 'log_path' variable to point to the appropriate directory on your server (i.e. /usr/local/papercut/logs/) - Modify the 'nntp_hostname' and 'nntp_port' variables to your appropriate server name and port number. (Note: if you want to run Papercut on port 199, you may need to be root depending on your system) - Choose your backend type by changing the 'backend_type' variable. (Note: as of version 0.7.1, there is only one backend -> phorum_mysql) - Finally, change the MySQL related variables so Papercut can connect to the appropriate database and get the content of the messages. 3) If your backend type is 'phorum_mysql': - You will need to add a new column under the main forum listing table to associate the name of a newsgroup to a table name. Since Phorum is totally dynamic on the number of forums it can create, we need an extra column to prevent problems. $ cd /path/to/where/papercut/will/run/ $ cd backends $ less phorum_mysql_fix.sql [read the information contained on the file] $ mysql -u username_here -p database_here < phorum_mysql_fix.sql [password will be requested now] - Now that the new column was created on the main forum listing table, you will need to edit it and enter the name of the newsgroup that you want for each forum. - After you finish editing the main forum table, you will need to go back to the settings.py file and configure the full path for the Phorum settings folder. That is, the folder where you keep the 'forums.php' configuration file and all other files that setup the options for each forum. It will usually have 'forums.php', '1.php', '2.php' and so on. The numbers on the filenames are actually the forum IDs on the main forum table. In any case, you will need to change the 'phorum_settings_path' variable on the settings.py file and put the full path to this folder. - You will also need to set the version of the installed copy of Phorum so Papercut can send the correct headers when sending out copies of the posted articles (also called PhorumMail for the Phorum lovers out there). Set the 'phorum_version' variable as appropriate on your case (i.e. '3.3.2a'). If you find any problems on this set of instructions, or if the instructions didn't work out for you, please let me know. papercut-0.9.13.orig/LICENSE0000644000175000017500000000207610327536616014435 0ustar lunarlunarCopyright (c) 2002 Joao Prado Maia Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. papercut-0.9.13.orig/papercut_cache.py0000644000175000017500000000563710327536616016756 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: papercut_cache.py,v 1.7 2002/10/04 03:14:38 jpm Exp $ import binascii import md5 import time import os import cPickle import portable_locker # papercut settings file import settings # methods that need to be cached cache_methods = ('get_XHDR', 'get_XGTITLE', 'get_LISTGROUP', 'get_XPAT', 'get_XOVER', 'get_BODY', 'get_HEAD', 'get_ARTICLE', 'get_STAT', 'get_LIST') class CallableWrapper: name = None thecallable = None cacheable_methods = () def __init__(self, name, thecallable, cacheable_methods): self.name = name self.thecallable = thecallable self.cacheable_methods = cacheable_methods def __call__(self, *args, **kwds): if self.name not in self.cacheable_methods: return self.thecallable(*args, **kwds) else: filename = self._get_filename(*args, **kwds) if os.path.exists(filename): # check the expiration expire, result = self._get_cached_result(filename) diff = time.time() - expire if diff > settings.nntp_cache_expire: # remove the file and run the method again return self._save_result(filename, *args, **kwds) else: return result else: return self._save_result(filename, *args, **kwds) def _get_cached_result(self, filename): inf = open(filename, 'rb') # get a lock on the file portable_locker.lock(inf, portable_locker.LOCK_SH) expire = cPickle.load(inf) result = cPickle.load(inf) # release the lock portable_locker.unlock(inf) inf.close() return (expire, result) def _save_result(self, filename, *args, **kwds): result = self.thecallable(*args, **kwds) # save the serialized result in the file outf = open(filename, 'w') # file write lock portable_locker.lock(outf, portable_locker.LOCK_EX) cPickle.dump(time.time(), outf) cPickle.dump(result, outf) # release the lock portable_locker.unlock(outf) outf.close() return result def _get_filename(self, *args, **kwds): arguments = '%s%s%s' % (self.name, args, kwds) return '%s%s' % (settings.nntp_cache_path, binascii.hexlify(md5.new(arguments).digest())) class Cache: backend = None cacheable_methods = () def __init__(self, storage_handle, cacheable_methods): self.backend = storage_handle.Papercut_Storage() self.cacheable_methods = cacheable_methods def __getattr__(self, name): result = getattr(self.backend, name) if callable(result): result = CallableWrapper(name, result, self.cacheable_methods) return result papercut-0.9.13.orig/portable_locker.py0000644000175000017500000000221210327536616017141 0ustar lunarlunar#!/usr/bin/env python # $Id: portable_locker.py,v 1.2 2002/10/03 01:05:24 jpm Exp $ # Note: this was originally from Python Cookbook, which was # probably taken from ASPN's Python Cookbook import os # needs win32all to work on Windows if os.name == 'nt': import win32con, win32file, pywintypes LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK LOCK_SH = 0 # the default LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY __overlapped = pywintypes.OVERLAPPED( ) def lock(fd, flags): hfile = win32file._get_osfhandle(fd.fileno( )) win32file.LockFileEx(hfile, flags, 0, 0xffff0000, __overlapped) def unlock(fd): hfile = win32file._get_osfhandle(fd.fileno( )) win32file.UnlockFileEx(hfile, 0, 0xffff0000, __overlapped) elif os.name == 'posix': import fcntl LOCK_EX = fcntl.LOCK_EX LOCK_SH = fcntl.LOCK_SH LOCK_NB = fcntl.LOCK_NB def lock(fd, flags): fcntl.flock(fd.fileno(), flags) def unlock(fd): fcntl.flock(fd.fileno(), fcntl.LOCK_UN) else: raise RuntimeError("portable_locker only defined for nt and posix platforms") papercut-0.9.13.orig/README0000644000175000017500000000360210327536616014304 0ustar lunarlunar-------------------- Papercut NNTP Server -------------------- Papercut is a news server written in 100% pure Python. It is intended to be extensible to the point where people can develop their own plug-ins and by that integrate the NNTP protocol to their applications. The server is compliant with most of the RFC0977 standards (when they make sense and are needed) and implements a lot of RFC1036 and RFC2980 extensions to the NNTP protocol. It was tested against Netscape News, Mozilla News and tin (under Solaris) and it works properly. The original need for this server was to integrate my PHP related web site forums with an NNTP gateway interface, so people could list and read the messages posted to the forums on their favorite News reader. The software on this case was Phorum (http://phorum.org) and the site is PHPBrasil.com (http://phpbrasil.com). At first it wasn't intended to support message posting, but it made sense to allow it after seeing how effective the tool was. The concept of storage modules was created exactly for this. I would create a Python class to handle the inner-workins of Phorum and MySQL and if I ever wanted to integrate the server with another type of software, I would just need to write a new storage module class. Anyway, back to the technical praise. Papercut is multi-threaded on Windows platforms and forking-based on UNIX platforms and should be reasonably fast (that means basically: 'it's pretty fast, but don't try serving 1000 connection at a time). The best thing about the application is that it is very simple to extend it. Papercut is licensed under the BSD license, which means you can sell it or do whatever you like with it. However, I ask that if you think Papercut is a good tool and you made a few enhancements to it or even fixed some bugs, please send me a patch or something. I will appreciate it :) -- Joao Prado Maia (jpm@papercut.org) papercut-0.9.13.orig/TODO0000644000175000017500000000104310327536616014111 0ustar lunarlunarTODO list: ---------- - Set the self.commands and self.extensions on the storage extensions, as some storages do not support all commands - MODE STREAM (it means several commands at the same time without waiting for responses) - Check more the patterns of searching (wildmat) -> backend.format_wildcards() -> Work in progress - Fork the server to the background automatically (using fork()?) - Make a command line option to make the server actually run on the foreground (-f option?) - Add a --verbose flag to replace the current __DEBUG__ flagpapercut-0.9.13.orig/papercut.py0000644000175000017500000010617210327536616015627 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: papercut.py,v 1.93 2005/03/05 04:06:54 jpm Exp $ import SocketServer import sys import os import signal import time import re import traceback import StringIO # papercut based modules import settings import papercut_cache __VERSION__ = '0.9.13' # set this to 0 (zero) for real world use __DEBUG__ = 0 # how many seconds to wait for data from the clients (draft 20 of the new NNTP protocol says at least 3 minutes) __TIMEOUT__ = 180 # some constants to hold the possible responses ERR_NOTCAPABLE = '500 command not recognized' ERR_CMDSYNTAXERROR = '501 command syntax error (or un-implemented option)' ERR_NOSUCHGROUP = '411 no such news group' ERR_NOGROUPSELECTED = '412 no newsgroup has been selected' ERR_NOARTICLESELECTED = '420 no current article has been selected' ERR_NOARTICLERETURNED = '420 No article(s) selected' ERR_NOPREVIOUSARTICLE = '422 no previous article in this group' ERR_NONEXTARTICLE = '421 no next article in this group' ERR_NOSUCHARTICLENUM = '423 no such article in this group' ERR_NOSUCHARTICLE = '430 no such article' ERR_NOIHAVEHERE = '435 article not wanted - do not send it' ERR_NOSTREAM = '500 Command not understood' ERR_TIMEOUT = '503 Timeout after %s seconds, closing connection.' ERR_NOTPERFORMED = '503 program error, function not performed' ERR_POSTINGFAILED = '441 Posting failed' ERR_AUTH_NO_PERMISSION = '502 No permission' ERR_NODESCAVAILABLE = '481 Groups and descriptions unavailable' STATUS_SLAVE = '202 slave status noted' STATUS_POSTMODE = '200 Hello, you can post' STATUS_NOPOSTMODE = '201 Hello, you can\'t post' STATUS_HELPMSG = '100 help text follows' STATUS_GROUPSELECTED = '211 %s %s %s %s group selected' STATUS_LIST = '215 list of newsgroups follows' STATUS_STAT = '223 %s %s article retrieved - request text separately' STATUS_ARTICLE = '220 %s %s All of the article follows' STATUS_NEWGROUPS = '231 list of new newsgroups follows' STATUS_NEWNEWS = '230 list of new articles by message-id follows' STATUS_HEAD = '221 %s %s article retrieved - head follows' STATUS_BODY = '222 %s %s article retrieved - body follows' STATUS_READYNOPOST = '201 %s Papercut %s server ready (no posting allowed)' STATUS_READYOKPOST = '200 %s Papercut %s server ready (posting allowed)' STATUS_CLOSING = '205 closing connection - goodbye!' STATUS_XOVER = '224 Overview information follows' STATUS_XPAT = '221 Header follows' STATUS_LISTGROUP = '211 %s %s %s %s Article numbers follow (multiline)' STATUS_XGTITLE = '282 list of groups and descriptions follows' STATUS_LISTNEWSGROUPS = '215 information follows' STATUS_XHDR = '221 Header follows' STATUS_DATE = '111 %s' STATUS_OVERVIEWFMT = '215 information follows' STATUS_EXTENSIONS = '215 Extensions supported by server.' STATUS_SENDARTICLE = '340 Send article to be posted' STATUS_READONLYSERVER = '440 Posting not allowed' STATUS_POSTSUCCESSFULL = '240 Article received ok' STATUS_AUTH_REQUIRED = '480 Authentication required' STATUS_AUTH_ACCEPTED = '281 Authentication accepted' STATUS_AUTH_CONTINUE = '381 More authentication information required' STATUS_SERVER_VERSION = '200 Papercut %s' % (__VERSION__) # the currently supported overview headers overview_headers = ('Subject', 'From', 'Date', 'Message-ID', 'References', 'Bytes', 'Lines', 'Xref') # we don't need to create the regular expression objects for every request, # so let's create them just once and re-use as needed newsgroups_regexp = re.compile("^Newsgroups:(.*)", re.M) contenttype_regexp = re.compile("^Content-Type:(.*);", re.M) authinfo_regexp = re.compile("AUTHINFO PASS") if os.name == 'posix': class NNTPServer(SocketServer.ForkingTCPServer): allow_reuse_address = 1 if settings.max_connections: max_children = settings.max_connections else: class NNTPServer(SocketServer.ThreadingTCPServer): allow_reuse_address = 1 class NNTPRequestHandler(SocketServer.StreamRequestHandler): # this is the list of supported commands commands = ('ARTICLE', 'BODY', 'HEAD', 'STAT', 'GROUP', 'LIST', 'POST', 'HELP', 'LAST','NEWGROUPS', 'NEWNEWS', 'NEXT', 'QUIT', 'MODE', 'XOVER', 'XPAT', 'LISTGROUP', 'XGTITLE', 'XHDR', 'SLAVE', 'DATE', 'IHAVE', 'OVER', 'HDR', 'AUTHINFO', 'XROVER', 'XVERSION') # this is the list of list of extensions supported that are obviously not in the official NNTP document extensions = ('XOVER', 'XPAT', 'LISTGROUP', 'XGTITLE', 'XHDR', 'MODE', 'OVER', 'HDR', 'AUTHINFO', 'XROVER', 'XVERSION') terminated = 0 selected_article = 'ggg' selected_group = 'ggg' tokens = [] sending_article = 0 article_lines = [] broken_oe_checker = 0 auth_username = '' def handle_timeout(self, signum, frame): self.terminated = 1 settings.logEvent('Connection timed out from %s' % (self.client_address[0])) def handle(self): settings.logEvent('Connection from %s' % (self.client_address[0])) if settings.server_type == 'read-only': self.send_response(STATUS_READYNOPOST % (settings.nntp_hostname, __VERSION__)) else: self.send_response(STATUS_READYOKPOST % (settings.nntp_hostname, __VERSION__)) while not self.terminated: if self.sending_article == 0: self.article_lines = [] if os.name == 'posix': signal.signal(signal.SIGALRM, self.handle_timeout) signal.alarm(__TIMEOUT__) try: self.inputline = self.rfile.readline() except IOError: continue if os.name == 'posix': signal.alarm(0) if __DEBUG__: print "client>", repr(self.inputline) # Strip spaces only if NOT receiving article if not self.sending_article: line = self.inputline.strip() else: line = self.inputline # somehow outlook express sends a lot of newlines (so we need to kill those users when this happens) if (not self.sending_article) and (line == ''): self.broken_oe_checker += 1 if self.broken_oe_checker == 10: self.terminated = 1 continue self.tokens = line.split(' ') # NNTP commands are case-insensitive command = self.tokens[0].upper() # don't save the password in the log file match = authinfo_regexp.search(line) if not match: settings.logEvent('Received request: %s' % (line)) if command == 'POST': if settings.server_type == 'read-only': settings.logEvent('Error - Read-only server received a post request from \'%s\'' % self.client_address[0]) self.send_response(STATUS_READONLYSERVER) else: if settings.nntp_auth == 'yes' and self.auth_username == '': self.send_response(STATUS_AUTH_REQUIRED) else: self.sending_article = 1 self.send_response(STATUS_SENDARTICLE) else: if settings.nntp_auth == 'yes' and self.auth_username == '' and command not in ('AUTHINFO', 'MODE'): self.send_response(STATUS_AUTH_REQUIRED) else: if self.sending_article: if self.inputline == '.\r\n': self.sending_article = 0 try: self.do_POST() except: # use a temporary file handle object to store the traceback information temp = StringIO.StringIO() traceback.print_exc(file=temp) temp_msg = temp.getvalue() # save on the log file settings.logEvent('Error - Posting failed for user from \'%s\' (exception triggered)' % self.client_address[0]) settings.logEvent(temp_msg) if __DEBUG__: print 'Error - Posting failed for user from \'%s\' (exception triggered; details below)' % self.client_address[0] print temp_msg self.send_response(ERR_POSTINGFAILED) continue self.article_lines.append(line) else: if command in self.commands: getattr(self, "do_%s" % (command))() else: self.send_response(ERR_NOTCAPABLE) settings.logEvent('Connection closed (IP Address: %s)' % (self.client_address[0])) def do_NEWGROUPS(self): """ Syntax: NEWGROUPS date time [GMT] [] Responses: 231 list of new newsgroups follows """ if (len(self.tokens) < 3) or (len(self.tokens) > 5): self.send_response(ERR_CMDSYNTAXERROR) return if (len(self.tokens) > 3) and (self.tokens[3] == 'GMT'): ts = self.get_timestamp(self.tokens[1], self.tokens[2], 'yes') else: ts = self.get_timestamp(self.tokens[1], self.tokens[2], 'no') groups = backend.get_NEWGROUPS(ts) if groups == None: msg = "%s\r\n." % (STATUS_NEWGROUPS) else: msg = "%s\r\n%s\r\n." % (STATUS_NEWGROUPS, groups) self.send_response(msg) def do_GROUP(self): """ Syntax: GROUP ggg Responses: 211 n f l s group selected (n = estimated number of articles in group, f = first article number in the group, l = last article number in the group, s = name of the group.) 411 no such news group """ # check the syntax of the command if len(self.tokens) != 2: self.send_response(ERR_CMDSYNTAXERROR) return # check to see if the group exists if not backend.group_exists(self.tokens[1]): self.send_response(ERR_NOSUCHGROUP) return self.selected_group = self.tokens[1] total_articles, first_art_num, last_art_num = backend.get_GROUP(self.tokens[1]) self.send_response(STATUS_GROUPSELECTED % (total_articles, first_art_num, last_art_num, self.tokens[1])) def do_NEWNEWS(self): """ Syntax: NEWNEWS newsgroups date time [GMT] [] Responses: 230 list of new articles by message-id follows """ # check the syntax of the command if (len(self.tokens) < 4) or (len(self.tokens) > 6): self.send_response(ERR_CMDSYNTAXERROR) return # check to see if the group exists if (self.tokens[1] != '*') and (not backend.group_exists(self.tokens[1])): self.send_response(ERR_NOSUCHGROUP) return if (len(self.tokens) > 4) and (self.tokens[4] == 'GMT'): ts = self.get_timestamp(self.tokens[2], self.tokens[3], 'yes') else: ts = self.get_timestamp(self.tokens[2], self.tokens[3], 'no') news = backend.get_NEWNEWS(ts, self.tokens[1]) if len(news) == 0: msg = "%s\r\n." % (STATUS_NEWNEWS) else: msg = "%s\r\n%s\r\n." % (STATUS_NEWNEWS, news) self.send_response(msg) def do_LIST(self): """ Syntax: LIST (done) LIST ACTIVE [wildmat] LIST ACTIVE.TIMES LIST DISTRIBUTIONS LIST DISTRIB.PATS LIST NEWSGROUPS [wildmat] LIST OVERVIEW.FMT (done) LIST SUBSCRIPTIONS LIST EXTENSIONS (not documented) (done by comparing the results of other servers) Responses: 215 list of newsgroups follows 503 program error, function not performed """ if (len(self.tokens) == 2) and (self.tokens[1].upper() == 'OVERVIEW.FMT'): self.send_response("%s\r\n%s:\r\n." % (STATUS_OVERVIEWFMT, ":\r\n".join(overview_headers))) return elif (len(self.tokens) == 2) and (self.tokens[1].upper() == 'EXTENSIONS'): self.send_response("%s\r\n%s\r\n." % (STATUS_EXTENSIONS, "\r\n".join(self.extensions))) return #elif (len(self.tokens) > 1) and (self.tokens[1].upper() == 'ACTIVE'): # lists = backend.get_LIST_ACTIVE(self.tokens[2]) # self.send_response("%s\r\n%s\r\n." % (STATUS_LIST, "\r\n".join(lists))) # return elif (len(self.tokens) > 1) and (self.tokens[1].upper() == 'NEWSGROUPS'): self.do_LIST_NEWSGROUPS() return elif len(self.tokens) == 2: self.send_response(ERR_NOTPERFORMED) return result = backend.get_LIST(self.auth_username) self.send_response("%s\r\n%s\r\n." % (STATUS_LIST, result)) def do_STAT(self): """ Syntax: STAT [nnn|] Responses: 223 n a article retrieved - request text separately (n = article number, a = unique article id) 412 no newsgroup selected 420 no current article has been selected 421 no next article in this group """ # check the syntax of the command if len(self.tokens) > 2: self.send_response(ERR_CMDSYNTAXERROR) return if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if len(self.tokens) == 1: # check if the currently selected article pointer is set if self.selected_article == 'ggg': self.send_response(ERR_NOARTICLESELECTED) return else: self.tokens.append(self.selected_article) report_article_number = self.tokens[1] else: # get the article number if it is the appropriate option if self.tokens[1].find('<') != -1: self.tokens[1] = self.get_number_from_msg_id(self.tokens[1]) report_article_number = 0 else: report_article_number = self.tokens[1] if not backend.get_STAT(self.selected_group, self.tokens[1]): self.send_response(ERR_NOSUCHARTICLENUM) return # only set the internally selected article if the article number variation is used if len(self.tokens) == 2 and self.tokens[1].find('<') == -1: self.selected_article = self.tokens[1] self.send_response(STATUS_STAT % (report_article_number, backend.get_message_id(self.tokens[1], self.selected_group))) def do_ARTICLE(self): """ Syntax: ARTICLE nnn| Responses: 220 n article retrieved - head and body follow (n = article number, = message-id) 221 n article retrieved - head follows 222 n article retrieved - body follows 223 n article retrieved - request text separately 412 no newsgroup has been selected 420 no current article has been selected 423 no such article number in this group 430 no such article found """ # check the syntax if len(self.tokens) != 2: self.send_response(ERR_CMDSYNTAXERROR) return if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return # get the article number if it is the appropriate option if self.tokens[1].find('<') != -1: self.tokens[1] = self.get_number_from_msg_id(self.tokens[1]) report_article_number = 0 else: report_article_number = self.tokens[1] result = backend.get_ARTICLE(self.selected_group, self.tokens[1]) if result == None: self.send_response(ERR_NOSUCHARTICLENUM) else: # only set the internally selected article if the article number variation is used if len(self.tokens) == 2 and self.tokens[1].find('<') == -1: self.selected_article = self.tokens[1] response = STATUS_ARTICLE % (report_article_number, backend.get_message_id(self.selected_article, self.selected_group)) self.send_response("%s\r\n%s\r\n\r\n%s\r\n." % (response, result[0], result[1])) def do_LAST(self): """ Syntax: LAST Responses: 223 n a article retrieved - request text separately (n = article number, a = unique article id) """ # check if there is a previous article if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if self.selected_article == 'ggg': self.send_response(ERR_NOARTICLESELECTED) return article_num = backend.get_LAST(self.selected_group, self.selected_article) if article_num == None: self.send_response(ERR_NOPREVIOUSARTICLE) return self.selected_article = article_num self.send_response(STATUS_STAT % (article_num, backend.get_message_id(article_num, self.selected_group))) def do_NEXT(self): """ Syntax: NEXT Responses: 223 n a article retrieved - request text separately (n = article number, a = unique article id) 412 no newsgroup selected 420 no current article has been selected 421 no next article in this group """ # check if there is a previous article if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if self.selected_article == 'ggg': article_num = backend.get_first_article(self.selected_group) else: article_num = backend.get_NEXT(self.selected_group, self.selected_article) if article_num == None: self.send_response(ERR_NONEXTARTICLE) return self.selected_article = article_num self.send_response(STATUS_STAT % (article_num, backend.get_message_id(article_num, self.selected_group))) def do_BODY(self): """ Syntax: BODY [nnn|] Responses: 222 10110 <23445@sdcsvax.ARPA> article retrieved - body follows (body text here) """ if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if ((len(self.tokens) == 1) and (self.selected_article == 'ggg')): self.send_response(ERR_NOARTICLESELECTED) return if len(self.tokens) == 2: if self.tokens[1].find('<') != -1: self.tokens[1] = self.get_number_from_msg_id(self.tokens[1]) article_number = self.tokens[1] body = backend.get_BODY(self.selected_group, self.tokens[1]) else: article_number = self.selected_article body = backend.get_BODY(self.selected_group, self.selected_article) if body == None: self.send_response(ERR_NOSUCHARTICLENUM) else: # only set the internally selected article if the article number variation is used if len(self.tokens) == 2 and self.tokens[1].find('<') == -1: self.selected_article = self.tokens[1] self.send_response("%s\r\n%s\r\n." % (STATUS_BODY % (article_number, backend.get_message_id(self.selected_article, self.selected_group)), body)) def do_HEAD(self): """ Syntax: HEAD [nnn|] Responses: 221 1013 <5734@mcvax.UUCP> Article retrieved; head follows. """ if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if ((len(self.tokens) == 1) and (self.selected_article == 'ggg')): self.send_response(ERR_NOARTICLESELECTED) return if len(self.tokens) == 2: if self.tokens[1].find('<') != -1: self.tokens[1] = self.get_number_from_msg_id(self.tokens[1]) article_number = self.tokens[1] head = backend.get_HEAD(self.selected_group, self.tokens[1]) else: article_number = self.selected_article head = backend.get_HEAD(self.selected_group, self.selected_article) if head == None: self.send_response(ERR_NOSUCHARTICLENUM) else: # only set the internally selected article if the article number variation is used if len(self.tokens) == 2 and self.tokens[1].find('<') == -1: self.selected_article = self.tokens[1] self.send_response("%s\r\n%s\r\n." % (STATUS_HEAD % (article_number, backend.get_message_id(self.selected_article, self.selected_group)), head)) def do_OVER(self): self.do_XOVER() def do_XOVER(self): """ Syntax: XOVER [range] Responses: 224 Overview information follows\r\n subject\tauthor\tdate\tmessage-id\treferences\tbyte count\tline count\r\n 412 No news group current selected 420 No article(s) selected """ if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return # check the command style if len(self.tokens) == 1: # only show the information for the current selected article if self.selected_article == 'ggg': self.send_response(ERR_NOARTICLESELECTED) return overviews = backend.get_XOVER(self.selected_group, self.selected_article, self.selected_article) else: if self.tokens[1].find('-') == -1: overviews = backend.get_XOVER(self.selected_group, self.tokens[1], self.tokens[1]) else: ranges = self.tokens[1].split('-') if ranges[1] == '': # this is a start-everything style of XOVER overviews = backend.get_XOVER(self.selected_group, ranges[0]) else: # this is a start-end style of XOVER overviews = backend.get_XOVER(self.selected_group, ranges[0], ranges[1]) if overviews == None: self.send_response(ERR_NOTCAPABLE) return if len(overviews) == 0: msg = "%s\r\n." % (STATUS_XOVER) else: msg = "%s\r\n%s\r\n." % (STATUS_XOVER, overviews) self.send_response(msg) def do_XPAT(self): """ Syntax: XPAT header range| pat [pat...] Responses: 221 Header follows 430 no such article 502 no permission """ if len(self.tokens) < 4: self.send_response(ERR_CMDSYNTAXERROR) return if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if not self.index_in_list(overview_headers, self.tokens[1]): self.send_response("%s\r\n." % (STATUS_XPAT)) return if self.tokens[2].find('@') != -1: self.tokens[2] = self.get_number_from_msg_id(self.tokens[2]) self.do_XHDR() return else: ranges = self.tokens[2].split('-') if ranges[1] == '': overviews = backend.get_XPAT(self.selected_group, self.tokens[1], self.tokens[3], ranges[0]) else: overviews = backend.get_XPAT(self.selected_group, self.tokens[1], self.tokens[3], ranges[0], ranges[1]) if overviews == None: self.send_response(ERR_NOTCAPABLE) return self.send_response("%s\r\n%s\r\n." % (STATUS_XPAT, overviews)) def do_LISTGROUP(self): """ Syntax: LISTGROUP [ggg] Responses: 211 list of article numbers follow 411 No such group 412 Not currently in newsgroup 502 no permission """ if len(self.tokens) > 2: self.send_response(ERR_CMDSYNTAXERROR) return if len(self.tokens) == 2: # check if the group exists if not backend.group_exists(self.tokens[1]): # the draft of the new NNTP protocol tell us to reply this instead of an empty list self.send_response(ERR_NOSUCHGROUP) return numbers = backend.get_LISTGROUP(self.tokens[1]) else: if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return numbers = backend.get_LISTGROUP(self.selected_group) check = numbers.split('\r\n') if len(check) > 0: # When a valid group is selected by means of this command, the # internally maintained "current article pointer" is set to the first # article in the group. self.selected_article = check[0] if len(self.tokens) == 2: self.selected_group = self.tokens[1] else: # If an empty newsgroup is selected, the current article pointer is made invalid. self.selected_article = 'ggg' self.send_response("%s\r\n%s\r\n." % (STATUS_LISTGROUP % (backend.get_group_stats(self.selected_group)), numbers)) def do_XGTITLE(self): """ Syntax: XGTITLE [wildmat] Responses: 481 Groups and descriptions unavailable 282 list of groups and descriptions follows """ if len(self.tokens) > 2: self.send_response(ERR_CMDSYNTAXERROR) return if len(self.tokens) == 2: info = backend.get_XGTITLE(self.tokens[1]) else: if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return info = backend.get_XGTITLE(self.selected_group) if info is None: self.send_response(ERR_NODESCAVAILABLE) elif len(info) == 0: self.send_response("%s\r\n." % (STATUS_XGTITLE)) else: self.send_response("%s\r\n%s\r\n." % (STATUS_XGTITLE, info)) def do_LIST_NEWSGROUPS(self): """ Syntax: LIST NEWSGROUPS [wildmat] Responses: 503 program error, function not performed 215 list of groups and descriptions follows """ if len(self.tokens) > 3: self.send_response(ERR_CMDSYNTAXERROR) return if len(self.tokens) == 3: info = backend.get_XGTITLE(self.tokens[2]) else: info = backend.get_XGTITLE() self.send_response("%s\r\n%s\r\n." % (STATUS_LISTNEWSGROUPS, info)) def do_HDR(self): self.do_XHDR() def do_XROVER(self): self.tokens[1] = 'REFERENCES' self.do_XHDR() def do_XHDR(self): """ Syntax: XHDR header [range|] Responses: 221 Header follows 412 No news group current selected 420 No current article selected 430 no such article """ if (len(self.tokens) < 2) or (len(self.tokens) > 3): self.send_response(ERR_CMDSYNTAXERROR) return if self.selected_group == 'ggg': self.send_response(ERR_NOGROUPSELECTED) return if (self.tokens[1].upper() != 'SUBJECT') and (self.tokens[1].upper() != 'FROM'): self.send_response(ERR_CMDSYNTAXERROR) return if len(self.tokens) == 2: if self.selected_article == 'ggg': self.send_response(ERR_NOARTICLESELECTED) return info = backend.get_XHDR(self.selected_group, self.tokens[1], 'unique', (self.selected_article)) else: # check the XHDR style now if self.tokens[2].find('@') != -1: self.tokens[2] = self.get_number_from_msg_id(self.tokens[2]) info = backend.get_XHDR(self.selected_group, self.tokens[1], 'unique', (self.tokens[2])) else: ranges = self.tokens[2].split('-') if ranges[1] == '': info = backend.get_XHDR(self.selected_group, self.tokens[1], 'range', (ranges[0])) else: info = backend.get_XHDR(self.selected_group, self.tokens[1], 'range', (ranges[0], ranges[1])) # check for empty results if info == None: self.send_response(ERR_NOTCAPABLE) else: self.send_response("%s\r\n%s\r\n." % (STATUS_XHDR, info)) def do_DATE(self): """ Syntax: DATE Responses: 111 YYYYMMDDhhmmss """ self.send_response(STATUS_DATE % (time.strftime('%Y%m%d%H%M%S', time.localtime(time.time())))) def do_HELP(self): """ Syntax: HELP Responses: 100 help text follows """ self.send_response("%s\r\n\t%s\r\n." % (STATUS_HELPMSG, "\r\n\t".join(self.commands))) def do_QUIT(self): """ Syntax: QUIT Responses: 205 closing connection - goodbye! """ self.terminated = 1 self.send_response(STATUS_CLOSING) def do_IHAVE(self): """ Syntax: IHAVE Responses: 235 article transferred ok 335 send article to be transferred. End with . 435 article not wanted - do not send it 436 transfer failed - try again later 437 article rejected - do not try again """ if (len(self.tokens) != 2) or (self.tokens[1].find('<') == -1): self.send_response(ERR_CMDSYNTAXERROR) return self.send_response(ERR_NOIHAVEHERE) def do_SLAVE(self): """ Syntax: SLAVE Responses: 202 slave status noted """ self.send_response(STATUS_SLAVE) def do_MODE(self): """ Syntax: MODE READER|STREAM Responses: 200 Hello, you can post 201 Hello, you can't post 203 Streaming is OK 500 Command not understood """ if self.tokens[1].upper() == 'READER': if settings.server_type == 'read-only': self.send_response(STATUS_NOPOSTMODE) else: self.send_response(STATUS_POSTMODE) elif self.tokens[1].upper() == 'STREAM': self.send_response(ERR_NOSTREAM) def do_POST(self): """ Syntax: POST Responses: 240 article posted ok 340 send article to be posted. End with . 440 posting not allowed 441 posting failed """ lines = "\r\n".join(self.article_lines) # check the 'Newsgroups' header group_name = newsgroups_regexp.search(lines, 0).groups()[0].strip() if not backend.group_exists(group_name): self.send_response(ERR_POSTINGFAILED) return result = backend.do_POST(group_name, lines, self.client_address[0], self.auth_username) if result == None: self.send_response(ERR_POSTINGFAILED) else: self.send_response(STATUS_POSTSUCCESSFULL) def do_AUTHINFO(self): """ Syntax: AUTHINFO USER username AUTHINFO PASS password Responses: 281 Authentication accepted 381 More authentication information required 480 Authentication required 482 Authentication rejected 502 No permission """ if len(self.tokens) != 3: self.send_response(ERR_CMDSYNTAXERROR) return if settings.nntp_auth == 'no': self.send_response(STATUS_AUTH_ACCEPTED) return if self.tokens[1].upper() == 'USER': self.auth_username = self.tokens[2] self.send_response(STATUS_AUTH_CONTINUE) elif self.tokens[1].upper() == 'PASS' and settings.nntp_auth == 'yes': if auth.is_valid_user(self.auth_username, self.tokens[2]): self.send_response(STATUS_AUTH_ACCEPTED) else: self.send_response(ERR_AUTH_NO_PERMISSION) self.auth_username = '' def do_XVERSION(self): self.send_response(STATUS_SERVER_VERSION) def get_number_from_msg_id(self, msg_id): return msg_id[1:msg_id.find('@')] def index_in_list(self, list, index): for item in list: if item.upper() == index.upper(): return 1 return 0 def get_timestamp(self, date, times, gmt='yes'): # like the new NNTP draft explains... if len(date) == 8: year = date[:4] else: local_year = str(time.localtime()[0]) if date[:2] > local_year[2:4]: year = "19%s" % (date[:2]) else: year = "20%s" % (date[:2]) ts = time.mktime((int(year), int(date[2:4]), int(date[4:6]), int(times[:2]), int(times[2:4]), int(times[4:6]), 0, 0, 0)) if gmt == 'yes': return time.gmtime(ts) else: return time.localtime(ts) def send_response(self, message): if __DEBUG__: print "server>", message self.wfile.write(message + "\r\n") self.wfile.flush() def finish(self): # cleaning up after ourselves self.terminated = 0 self.selected_article = 'ggg' self.selected_group = 'ggg' self.tokens = [] self.sending_article = 0 self.auth_username = '' self.article_lines = [] self.wfile.flush() self.wfile.close() self.rfile.close() if __DEBUG__: print 'Closing the request' if __name__ == '__main__': # set up signal handler def sighandler(signum, frame): if __DEBUG__: print "\nShutting down papercut..." server.socket.close() time.sleep(1) sys.exit(0) # dynamic loading of the appropriate storage backend module temp = __import__('storage.%s' % (settings.storage_backend), globals(), locals(), ['Papercut_Storage']) if settings.nntp_cache == 'yes': backend = papercut_cache.Cache(temp, papercut_cache.cache_methods) else: backend = temp.Papercut_Storage() # now for the authentication module, if needed if settings.nntp_auth == 'yes': temp = __import__('auth.%s' % (settings.auth_backend), globals(), locals(), ['Papercut_Auth']) auth = temp.Papercut_Auth() signal.signal(signal.SIGINT, sighandler) print 'Papercut %s (%s storage module) - starting up' % (__VERSION__, settings.storage_backend) server = NNTPServer((settings.nntp_hostname, settings.nntp_port), NNTPRequestHandler) server.serve_forever() papercut-0.9.13.orig/settings.py0000644000175000017500000000576510327536616015652 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: settings.py,v 1.18 2004/08/01 01:03:22 jpm Exp $ import time import sys import os # # The following configuration settings should be pretty self-explanatory, but # please let me know if this is not complete or if more information / examples # are needed. # # what is the maximum number of concurrent connections that should be allowed max_connections = 20 # # GENERAL PATH INFORMATION # # full path for where Papercut will store the log file log_path = "/home/papercut/logs/" # the actual log filename log_file = log_path + "papercut.log" # # HOSTNAME / PORT OF THE SERVER # # hostname that Papercut will bind against nntp_hostname = 'nntp.domain.com' # usually 119, but use 563 for an SSL server nntp_port = 119 # type of server ('read-only' or 'read-write') server_type = 'read-write' # # NNTP AUTHENTICATION SUPPORT # # does the server need authentication ? ('yes' or 'no') nntp_auth = 'no' # backend that Papercut will use to authenticate the users auth_backend = '' # ONLY needed for phorum_mysql_users auth module PHP_CRYPT_SALT_LENGTH = 2 # # CACHE SYSTEM # # the cache system may need a lot of diskspace ('yes' or 'no') nntp_cache = 'no' # cache expire (in seconds) nntp_cache_expire = 60 * 60 * 3 # path to where the cached files should be kept nntp_cache_path = '/home/papercut/cache/' # # STORAGE MODULE # # backend that Papercut will use to get (and store) the actual articles content storage_backend = "phorum_mysql" # for the forwarding_proxy backend, set the next option to the remote nntp server forward_host = 'news.remotedomain.com' # # PHORUM STORAGE MODULE OPTIONS # # full path to the directory where the Phorum configuration files are stored phorum_settings_path = "/home/papercut/www/domain.com/phorum_settings/" # the version for the installed copy of Phorum phorum_version = "3.3.2a" # configuration values for 'storage/phorum_mysql.py' # database connection variables dbhost = "localhost" dbname = "phorum" dbuser = "anonymous" dbpass = "anonymous" # # PHPBB STORAGE MODULE OPTIONS # # the prefix for the phpBB tables phpbb_table_prefix = "phpbb_" # # PHPNUKE PHPBB STORAGE MODULE OPTIONS # # if you're running PHPNuke, set this for the nuke tables and phpbb_table_prefix # for the bb tables. nuke_table_prefix = "nuke_" # the prefix for the phpBB tables phpbb_table_prefix = "nuke_bb" # # MBOX STORAGE MODULE OPTIONS # # the full path for where the mbox files are stored in mbox_path = "/home/papercut/mboxes/" # check for the appropriate options if nntp_auth == 'yes' and auth_backend == '': sys.exit("Please configure the 'nntp_auth' and 'auth_backend' options correctly") # check for the trailing slash if phorum_settings_path[-1] != '/': phorum_settings_path = phorum_settings_path + '/' # helper function to log information def logEvent(msg): f = open(log_file, "a") f.write("[%s] %s\n" % (time.strftime("%a %b %d %H:%M:%S %Y", time.gmtime()), msg)) f.close() papercut-0.9.13.orig/storage/0000755000175000017500000000000010327536617015070 5ustar lunarlunarpapercut-0.9.13.orig/storage/forwarding_proxy.py0000644000175000017500000001265010327536616021050 0ustar lunarlunar#!/usr/bin/env python import nntplib import re import time import StringIO # We need setting.forward_host, which is the nntp server we forward to import settings # This is an additional backend for Papercut, currently it's more or less proof-of-concept. # It's a "forwarding proxy", that merely forwards all requests to a "real" NNTP server. # Just for fun, the post command adds an additional header. # # Written by Gerhard Häring (gerhard@bigfoot.de) def log(s): # For debugging, replace with "pass" if this gets stable one day print s class Papercut_Storage: def __init__(self): self.nntp = nntplib.NNTP(settings.forward_host) def group_exists(self, group_name): try: self.nntp.group(group_name) except nntplib.NNTPTemporaryError, reason: return 0 return 1 def get_first_article(self, group_name): log(">get_first_article") # Not implemented return 1 def get_group_stats(self, container): # log(">get_group_stats") # Returns: (total, maximum, minimum) max, min = container return (max-min, max, min) def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_NEWGROUPS(self, ts, group='%'): log(">get_NEWGROUPS") date = time.strftime("%y%m%d", ts) tim = time.strftime("%H%M%S", ts) response, groups = self.nntp.newgroups(date, tim) return "\r\n".join(["%s" % k for k in (1,2,3)]) def get_NEWNEWS(self, ts, group='*'): log(">get_NEWNEWS") articles = [] articles.append("<%s@%s>" % (id, group)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): # Returns: (total, first_id, last_id) log(">get_GROUP") response, count, first, last, name = self.nntp.group(group_name) return (count, first, last) def get_LIST(self, username=""): # Returns: list of (groupname, table) log(">get_LIST") response, lst= self.nntp.list() def convert(x): return x[0], (int(x[1]), int(x[2])) lst = map(convert, lst) return lst def get_STAT(self, group_name, id): log(">get_STAT") try: resp, nr, id = self.nntp.stat(id) return nr except nntplib.NNTPTemporaryError, reason: return None def get_ARTICLE(self, group_name, id): log(">get_ARTICLE") resp, nr, id, headerlines = self.nntp.head(id) resp, nr, id, articlelines = self.nntp.article(id) dobreak = 0 while 1: if articlelines[0] == "": dobreak = 1 del articlelines[0] if dobreak: break return ("\r\n".join(headerlines), "\n".join(articlelines)) def get_LAST(self, group_name, current_id): log(">get_LAST") # Not implemented return None def get_NEXT(self, group_name, current_id): log(">get_NEXT") # Not implemented return None def get_HEAD(self, group_name, id): log(">get_HEAD") resp, nr, mid, headerlines = self.nntp.head(id) return "\r\n".join(headerlines) def get_BODY(self, group_name, id): log(">get_BODY") resp, nr, mid, bodylines = self.nntp.body(id) return "\r\n".join(bodylines) def get_XOVER(self, group_name, start_id, end_id='ggg'): # subject\tauthor\tdate\tmessage-id\treferences\tbyte count\tline count\r\n log(">get_XOVER") xov = list(self.nntp.xover(start_id, end_id)[1]) nxov = [] for entry in xov: entry = list(entry) entry[5] = "\n".join(entry[5]) nxov.append("\t".join(entry)) return "\r\n".join(nxov) def get_LIST_ACTIVE(self, pat): log(">get_LIST_ACTIVE") resp, list = self.nntp.longcmd('LIST ACTIVE %s' % pat) return list def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): log(">get_XPAT") return None def get_LISTGROUP(self, group_name=""): log(">get_LISTGROUP") ids = [] self.nntp.putcmd("LISTGROUP %s" % group_name) while 1: curline = self.nntp.getline() if curline == ".": break ids.append(curline) return "\r\n".join(ids) def get_XGTITLE(self, pattern="*"): log(">get_XGTITLE") resp, result = self.nntp.xgtitle(pattern) return "\r\n".join(["%s %s" % (group, title) for group, title in result]) def get_XHDR(self, group_name, header, style, range): log(">get_XHDR") if style == "range": range = "-".join(range) resp, result = self.nntp.xhdr(header, range) result = map(lambda x: x[1], result) return "\r\n".join(result) def do_POST(self, group_name, lines, ip_address, username=''): log(">do_POST") while lines.find("\r") > 0: lines = lines.replace("\r", "") lns = lines.split("\n") counter = 0 for l in lns: if l == "": lns.insert(counter, "X-Modified-By: Papercut's forwarding backend") break counter +=1 lines = "\n".join(lns) # we need to send an actual file f = StringIO.StringIO(lines) result = self.nntp.post(f) return resultpapercut-0.9.13.orig/storage/maildir.py0000644000175000017500000003016110327536616017063 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2004 Scott Parish, Joao Prado Maia # See the LICENSE file for more information. # $Id: maildir.py,v 1.2 2004/08/01 01:51:48 jpm Exp $ # # Maildir backend for papercut # # Notes: # # Currently the numeric message ids are based off the number of # files in that group's directy. This means that if you change # a file name, or delete a file you are going to change ids, which # in turn is going to confuse nntp clients! # # To add a new group: # mkdir -p /home/papercut/maildir/my.new.group/{new,cur,tmp} # import dircache from fnmatch import fnmatch import glob import os import mailbox import rfc822 import settings import socket import strutil import string import time def maildir_date_cmp(a, b): """compare maildir file names 'a' and 'b' for sort()""" a = os.path.basename(a) b = os.path.basename(b) a = int(a[: a.find(".")]) b = int(b[: b.find(".")]) return cmp(a, b) class Papercut_Storage: """ Storage backend interface for mbox files """ _proc_post_count = 0 def __init__(self, group_prefix="papercut.maildir."): self.maildir_dir = settings.maildir_path self.group_prefix = group_prefix def _get_group_dir(self, group): return os.path.join(self.maildir_dir, group) def _groupname2group(self, group_name): return group_name.replace(self.group_prefix, '') def _group2groupname(self, group): return self.group_prefix + group def _new_to_cur(self, group): groupdir = self._get_group_dir(group) for f in dircache.listdir(os.path.join(groupdir, 'new')): ofp = os.path.join(groupdir, 'new', f) nfp = os.path.join(groupdir, 'cur', f + ":2,") os.rename(ofp, nfp) def get_groupname_list(self): groups = dircache.listdir(self.maildir_dir) return ["papercut.maildir.%s" % k for k in groups] def get_group_article_list(self, group): self._new_to_cur(group) groupdir = self._get_group_dir(group) articledir = os.path.join(self._get_group_dir(group), 'cur') articles = dircache.listdir(articledir) articles.sort(maildir_date_cmp) return articles def get_group_article_count(self, group): self._new_to_cur(group) articles = dircache.listdir(os.path.join(self.maildir_dir, group)) return len(articles) def group_exists(self, group_name): groupnames = self.get_groupname_list() found = False for name in groupnames: # group names are supposed to be case insensitive if string.lower(name) == string.lower(group_name): found = True break return found def get_first_article(self, group_name): return 1 def get_group_stats(self, group_name): total, max, min = self.get_maildir_stats(group_name) return (total, min, max, group_name) def get_maildir_stats(self, group_name): cnt = len(self.get_group_article_list(group_name)) return cnt, cnt, 1 def get_message_id(self, msg_num, group_name): msg_num = int(msg_num) group = self._groupname2group(group_name) return '<%s@%s>' % (self.get_group_article_list(group)[msg_num - 1], group_name) def get_NEWGROUPS(self, ts, group='%'): return None # UNTESTED def get_NEWNEWS(self, ts, group='*'): gpaths = glob.glob(os.path.join(self.maildir_dir, group)) articles = [] for gpath in gpaths: articles = dircache.listdir(os.path.join(gpath, "cur")) group = os.path.basename(gpath) group_name = self._group2groupname(group) for article in articles: apath = os.path.join(gpath, "cur", article) if os.path.getmtime(apath) < ts: continue articles.append("<%s@%s" % (article, group_name)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): group = self._groupname2group(group_name) result = self.get_maildir_stats(group) return (result[0], result[2], result[1]) def get_LIST(self, username=""): result = self.get_groupname_list() if len(result) == 0: return "" else: groups = [] mutable = ('y', 'n')[settings.server_type == 'read-only'] for group_name in result: group = self._groupname2group(group_name) total, maximum, minimum = self.get_maildir_stats(group) groups.append("%s %s %s %s" % (group_name, maximum, minimum, mutable)) return "\r\n".join(groups) def get_STAT(self, group_name, id): # check if the message exists id = int(id) group = self._groupname2group(group_name) return id <= self.get_group_article_count(group) def get_message(self, group_name, id): group = self._groupname2group(group_name) id = int(id) try: article = self.get_group_article_list(group)[id - 1] file = os.path.join(self.maildir_dir, group, "cur", article) return rfc822.Message(open(file)) except IndexError: return None def get_ARTICLE(self, group_name, id): msg = self.get_message(group_name, id) if not msg: return None return ("\r\n".join(["%s" % string.strip(k) for k in msg.headers]), msg.fp.read()) def get_LAST(self, group_name, current_id): if current_id <= 1: return None return current_id - 1 def get_NEXT(self, group_name, current_id): group = self._groupname2group(group_name) if current_id >= self.get_group_article_count(group): return None return current_id + 1 def get_HEAD(self, group_name, id): msg = self.get_message(group_name, id) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (msg.get('from'))) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (msg.get('date'))) headers.append("Subject: %s" % (msg.get('subject'))) headers.append("Message-ID: <%s@%s>" % (id, group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, id)) return "\r\n".join(headers) def get_BODY(self, group_name, id): msg = self.get_message(group_name, id) if msg is None: return None else: return strutil.format_body(msg.fp.read()) def get_XOVER(self, group_name, start_id, end_id='ggg'): group = self._groupname2group(group_name) start_id = int(start_id) if end_id == 'ggg': end_id = self.get_group_article_count(group) else: end_id = int(end_id) overviews = [] for id in range(start_id, end_id + 1): msg = self.get_message(group_name, id) if msg is None: break author = msg.get('from') formatted_time = msg.get('date') message_id = self.get_message_id(id, group_name) line_count = len(msg.fp.read().split('\n')) xref = 'Xref: %s %s:%d' % (settings.nntp_hostname, group_name, id) if msg.get('references') is not None: reference = msg.get('references') else: reference = "" # message_number subject author date # message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % \ (id, msg.get('subject'), author, formatted_time, message_id, reference, len(strutil.format_body(msg.fp.read())), line_count, xref)) return "\r\n".join(overviews) # UNTESTED def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): group = self._groupname2group(group_name) header = header.upper() start_id = int(start_id) if end_id == 'ggg': end_id = self.get_group_article_count(group) else: end_id = int(end_id) hdrs = [] for id in range(start_id, end_id + 1): if header == 'MESSAGE-ID': msg_id = self.get_message_id(id, group_name) if fnmatch(msg_id, pattern): hdrs.append('%d %s' % (id, msg_id)) continue elif header == 'XREF': xref = '%s %s:%d' % (settings.nntp_hostname, group_name, id) if fnmatch(xref, pattern): hdrs.append('%d %s' % (id, xref)) continue msg = self.get_message(group_name, id) if header == 'BYTES': msg.fp.seek(0, 2) bytes = msg.fp.tell() if fnmatch(str(bytes), pattern): hdrs.append('%d %d' % (id, bytes)) elif header == 'LINES': lines = len(msg.fp.readlines()) if fnmatch(str(lines), pattern): hdrs.append('%d %d' % (id, lines)) else: hdr = msg.get(header) if hdr and fnmatch(hdr, pattern): hdrs.append('%d %s' % (id, hdr)) if len(hdrs): return "\r\n".join(hdrs) else: return "" def get_LISTGROUP(self, group_name): ids = range(1, self.get_group_article_count(group) + 1) ids = [str(id) for id in ids] return "\r\n".join(ids) def get_XGTITLE(self, pattern=None): # XXX no support for this right now return '' def get_XHDR(self, group_name, header, style, ranges): print group_name, header, style, ranges group = self._groupname2group(group_name) header = header.upper() if style == 'range': if len(ranges) == 2: range_end = int(ranges[1]) else: range_end = self.get_group_article_count(group) ids = range(int(ranges[0]), range_end + 1) else: ids = (int(ranges[0])) hdrs = [] for id in ids: if header == 'MESSAGE-ID': hdrs.append('%d %s' % \ (id, self.get_message_id(id, group_name))) continue elif header == 'XREF': hdrs.append('%d %s %s:%d' % (id, settings.nntp_hostname, group_name, id)) continue msg = self.get_message(group_name, id) if header == 'BYTES': msg.fp.seek(0, 2) hdrs.append('%d %d' % (id, msg.fp.tell())) elif header == 'LINES': hdrs.append('%d %d' % (id, len(msg.fp.readlines()))) else: hdr = msg.get(header) if hdr: hdrs.append('%d %s' % (id, hdr)) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, body, ip_address, username=''): self._proc_post_count += 1 count = self._proc_post_count ts = [int(x) for x in str(time.time()).split(".")] file = "%d.M%dP%dQ%d.%s" % (ts[0], ts[1], os.getpid(), count, socket.gethostname()) group = self._groupname2group(group_name) groupdir = self._get_group_dir(group) tfpath = os.path.join(self.maildir_dir, groupdir, "tmp", file) nfpath = os.path.join(self.maildir_dir, groupdir, "new", file) fd = open(tfpath, 'w') fd.write(body) fd.close os.rename(tfpath, nfpath) return 1 papercut-0.9.13.orig/storage/mbox.py0000644000175000017500000001564210327536616016416 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: mbox.py,v 1.7 2004/08/01 01:51:48 jpm Exp $ import os import mailbox import settings import strutil import string class Papercut_Storage: """ Storage backend interface for mbox files """ mbox_dir = '' def __init__(self): self.mbox_dir = settings.mbox_path def get_mailbox(self, filename): return mailbox.PortableUnixMailbox(open(self.mbox_dir + filename)) def get_file_list(self): return os.listdir(self.mbox_dir) def get_group_list(self): groups = self.get_file_list() return ["papercut.mbox.%s" % k for k in groups] def group_exists(self, group_name): groups = self.get_group_list() found = False for name in groups: # group names are supposed to be case insensitive if string.lower(name) == string.lower(group_name): found = True break return found def get_first_article(self, group_name): return 1 def get_group_stats(self, filename): total, max, min = self.get_mbox_stats(filename) return (total, min, max, filename) def get_mbox_stats(self, filename): mbox = self.get_mailbox(filename) dir(mbox) cnt = 0 while mbox.next(): cnt = cnt + 1 return (cnt-1, cnt, 1) def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_NEWGROUPS(self, ts, group='%'): # XXX: eventually add some code in here to get the mboxes newer than the given timestamp return None def get_NEWNEWS(self, ts, group='*'): return '' def get_GROUP(self, group_name): result = self.get_mbox_stats(group_name.replace('papercut.mbox.', '')) return (result[0], result[2], result[1]) def get_LIST(self, username=""): result = self.get_file_list() if len(result) == 0: return "" else: groups = [] for mbox in result: total, maximum, minimum = self.get_mbox_stats(mbox) if settings.server_type == 'read-only': groups.append("papercut.mbox.%s %s %s n" % (mbox, maximum, minimum)) else: groups.append("papercut.mbox.%s %s %s y" % (mbox, maximum, minimum)) return "\r\n".join(groups) def get_STAT(self, group_name, id): # check if the message exists mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) i = 0 while mbox.next(): if i == int(id): return True i = i + 1 return False def get_ARTICLE(self, group_name, id): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) i = 0 while 1: msg = mbox.next() if msg is None: return None if i == int(id): return ("\r\n".join(["%s" % string.strip(k) for k in msg.headers]), msg.fp.read()) i = i + 1 def get_LAST(self, group_name, current_id): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) if current_id == 1: return None else: i = 0 while 1: msg = mbox.next() if msg is None: return None if (i+1) == current_id: return i i = i + 1 def get_NEXT(self, group_name, current_id): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) print repr(current_id) i = 0 while 1: msg = mbox.next() if msg is None: return None if i > current_id: return i i = i + 1 def get_message(self, group_name, id): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) i = 0 while 1: msg = mbox.next() if msg is None: return None if i == int(id): return msg i = i + 1 def get_HEAD(self, group_name, id): msg = self.get_message(group_name, id) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (msg.get('from'))) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (msg.get('date'))) headers.append("Subject: %s" % (msg.get('subject'))) headers.append("Message-ID: <%s@%s>" % (id, group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, id)) return "\r\n".join(headers) def get_BODY(self, group_name, id): msg = self.get_message(group_name, id) if msg is None: return None else: return strutil.format_body(msg.fp.read()) def get_XOVER(self, group_name, start_id, end_id='ggg'): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) # don't count the first message mbox.next() i = 1 overviews = [] while 1: msg = mbox.next() if msg is None: break author = msg.get('from') formatted_time = msg.get('date') message_id = msg.get('message-id') line_count = len(msg.fp.read().split('\n')) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, i) if msg.get('in-reply-to') is not None: reference = msg.get('in-reply-to') else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (i, msg.get('subject'), author, formatted_time, message_id, reference, len(strutil.format_body(msg.fp.read())), line_count, xref)) i = i + 1 return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): # no support for this right now return None def get_LISTGROUP(self, group_name): mbox = self.get_mailbox(group_name.replace('papercut.mbox.', '')) # don't count the first message mbox.next() i = 0 ids = [] while 1: msg = mbox.next() if msg is None: break i = i + 1 ids.append(i) return "\r\n".join(ids) def get_XGTITLE(self, pattern=None): # no support for this right now return None def get_XHDR(self, group_name, header, style, range): # no support for this right now return None def do_POST(self, group_name, lines, ip_address, username=''): # let's make the mbox storage always read-only for now return None papercut-0.9.13.orig/storage/mysql_storage.sql0000644000175000017500000000274610327536616020512 0ustar lunarlunarDROP TABLE IF EXISTS papercut_groups; CREATE TABLE papercut_groups ( id int(10) unsigned NOT NULL auto_increment, name varchar(50) NOT NULL default '', active smallint(6) NOT NULL default '0', description varchar(255) NOT NULL default '', table_name varchar(50) NOT NULL default '', PRIMARY KEY (id), KEY name (name), KEY active (active) ) TYPE=MyISAM; DROP TABLE IF EXISTS papercut_groups_auth; CREATE TABLE papercut_groups_auth ( id int(10) unsigned NOT NULL auto_increment, sess_id varchar(32) NOT NULL default '', name varchar(50) NOT NULL default '', username varchar(50) NOT NULL default '', password varchar(50) NOT NULL default '', PRIMARY KEY (id), KEY name (name), KEY username (username) ) TYPE=MyISAM; DROP TABLE IF EXISTS papercut_default_table; CREATE TABLE papercut_default_table ( id int(10) unsigned NOT NULL default '0', datestamp datetime NOT NULL default '0000-00-00 00:00:00', thread int(10) unsigned NOT NULL default '0', parent int(10) unsigned NOT NULL default '0', author varchar(255) NOT NULL default '', subject varchar(255) NOT NULL default '', message_id varchar(255) NOT NULL default '', bytes int(10) unsigned NOT NULL default '0', line_num int(10) unsigned NOT NULL default '0', host varchar(15) NOT NULL default '', body text NOT NULL, PRIMARY KEY (id), KEY author (author), KEY datestamp (datestamp), KEY subject (subject), KEY thread (thread), KEY parent (parent) ) TYPE=MyISAM; papercut-0.9.13.orig/storage/mime.py0000644000175000017500000000264010327536616016372 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: mime.py,v 1.1 2002/02/03 06:15:25 jpm Exp $ import re import email def get_body(subpart): doubleline_regexp = re.compile("^\.\.", re.M) body = [] found = 0 raw_headers = subpart.split('\r\n') for line in raw_headers: if not found and line == '': found = 1 continue if found: body.append(doubleline_regexp.sub(".", line)) return "\r\n".join(body) def get_text_message(msg_string): msg = email.message_from_string(msg_string) cnt_type = msg.get_main_type() if cnt_type == 'text': # a simple mime based text/plain message (is this even possible?) body = get_body(msg_string) elif cnt_type == 'multipart': # needs to loop thru all parts and get the text version #print 'several parts here' text_parts = {} for part in msg.walk(): if part.get_main_type() == 'text': #print 'text based part' #print part.as_string() text_parts[part.get_params()[0][0]] = get_body(part.as_string()) if 'text/plain' in text_parts: return text_parts['text/plain'] elif 'text/html' in text_parts: return text_parts['text/html'] else: # not mime based body = get_body(msg_string) return body papercut-0.9.13.orig/storage/phorum_mysql.py0000644000175000017500000006731210327536616020211 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: phorum_mysql.py,v 1.47 2004/08/01 01:51:48 jpm Exp $ import MySQLdb import time from mimify import mime_encode_header, mime_decode_header import re import settings import mime import strutil import smtplib import md5 # patch by Andreas Wegmann to fix the handling of unusual encodings of messages q_quote_multiline = re.compile("=\?(.*?)\?[qQ]\?(.*?)\?=.*?=\?\\1\?[qQ]\?(.*?)\?=", re.M | re.S) # we don't need to compile the regexps everytime.. doubleline_regexp = re.compile("^\.\.", re.M) singleline_regexp = re.compile("^\.", re.M) from_regexp = re.compile("^From:(.*)<(.*)>", re.M) subject_regexp = re.compile("^Subject:(.*)", re.M) references_regexp = re.compile("^References:(.*)<(.*)>", re.M) lines_regexp = re.compile("^Lines:(.*)", re.M) # phorum configuration files related regexps moderator_regexp = re.compile("(.*)PHORUM\['ForumModeration'\](.*)='(.*)';", re.M) url_regexp = re.compile("(.*)PHORUM\['forum_url'\](.*)='(.*)';", re.M) admin_regexp = re.compile("(.*)PHORUM\['admin_url'\](.*)='(.*)';", re.M) server_regexp = re.compile("(.*)PHORUM\['forum_url'\](.*)='(.*)http://(.*)/(.*)';", re.M) mail_code_regexp = re.compile("(.*)PHORUM\['PhorumMailCode'\](.*)=(.*)'(.*)';", re.M) class Papercut_Storage: """ Storage Backend interface for the Phorum web message board software (http://phorum.org) This is the interface for Phorum running on a MySQL database. For more information on the structure of the 'storage' package, please refer to the __init__.py available on the 'storage' sub-directory. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def get_message_body(self, headers): """Parses and returns the most appropriate message body possible. The function tries to extract the plaintext version of a MIME based message, and if it is not available then it returns the html version. """ return mime.get_text_message(headers) def quote_string(self, text): """Quotes strings the MySQL way.""" return text.replace("'", "\\'") def group_exists(self, group_name): stmt = """ SELECT COUNT(*) AS total FROM forums WHERE LOWER(nntp_group_name)=LOWER('%s')""" % (group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def article_exists(self, group_name, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT COUNT(*) AS total FROM %s WHERE approved='Y'""" % (table_name) if style == 'range': stmt = "%s AND id > %s" % (stmt, range[0]) if len(range) == 2: stmt = "%s AND id < %s" % (stmt, range[1]) else: stmt = "%s AND id = %s" % (stmt, range[0]) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_first_article(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT IF(MIN(id) IS NULL, 0, MIN(id)) AS first_article FROM %s WHERE approved='Y'""" % (table_name) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_group_stats(self, group_name): total, max, min = self.get_table_stats(self.get_table_name(group_name)) return (total, min, max, group_name) def get_table_stats(self, table_name): stmt = """ SELECT COUNT(id) AS total, IF(MAX(id) IS NULL, 0, MAX(id)) AS maximum, IF(MIN(id) IS NULL, 0, MIN(id)) AS minimum FROM %s WHERE approved='Y'""" % (table_name) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone() def get_table_name(self, group_name): stmt = """ SELECT table_name FROM forums WHERE nntp_group_name='%s'""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_notification_emails(self, forum_id): # open the configuration file fp = open("%s%s.php" % (settings.phorum_settings_path, forum_id), "r") content = fp.read() fp.close() # get the value of the configuration variable recipients = [] mod_code = moderator_regexp.search(content, 0).groups() if mod_code[2] == 'r' or mod_code[2] == 'a': # get the moderator emails from the forum_auth table stmt = """ SELECT email FROM forums_auth, forums_moderators WHERE user_id=id AND forum_id=%s""" % (forum_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) for row in result: recipients.append(row[0]) return recipients def send_notifications(self, group_name, msg_id, thread_id, parent_id, msg_author, msg_email, msg_subject, msg_body): msg_tpl = """From: Phorum <%(recipient)s> To: %(recipient)s Subject: Moderate for %(forum_name)s at %(phorum_server_hostname)s Message: %(msg_id)s. Subject: %(msg_subject)s Author: %(msg_author)s Message: %(phorum_url)s/read.php?f=%(forum_id)s&i=%(msg_id)s&t=%(thread_id)s&admview=1 %(msg_body)s To delete this message use this URL: %(phorum_admin_url)s?page=easyadmin&action=del&type=quick&id=%(msg_id)s&num=1&thread=%(thread_id)s To edit this message use this URL: %(phorum_admin_url)s?page=edit&srcpage=easyadmin&id=%(msg_id)s&num=1&mythread=%(thread_id)s """ # get the forum_id for this group_name stmt = """ SELECT id, name FROM forums WHERE nntp_group_name='%s'""" % (group_name) self.cursor.execute(stmt) forum_id, forum_name = self.cursor.fetchone() # open the main configuration file fp = open("%sforums.php" % (settings.phorum_settings_path), "r") content = fp.read() fp.close() # regexps to get the content from the phorum configuration files phorum_url = url_regexp.search(content, 0).groups()[2] phorum_admin_url = admin_regexp.search(content, 0).groups()[2] phorum_server_hostname = server_regexp.search(content, 0).groups()[3] # connect to the SMTP server smtp = smtplib.SMTP('localhost') emails = self.get_notification_emails(forum_id) for recipient in emails: current_msg = msg_tpl % vars() smtp.sendmail("Phorum <%s>" % (recipient), recipient, current_msg) # XXX: Coding blind here. I really don't know much about how Phorum works with # XXX: sending forum postings as emails, but it's here. Let's call this a # XXX: temporary implementation. Should work fine, I guess. phorum_mail_code = mail_code_regexp.search(content, 0).groups()[3] notification_mail_tpl = """Message-ID: <%(random_msgid)s@%(phorum_server_hostname)s> From: %(msg_author)s %(msg_email)s Subject: %(msg_subject)s To: %(forum_name)s <%(email_list)s> Return-Path: <%(email_return)s> Reply-To: %(email_return)s X-Phorum-%(phorum_mail_code)s-Version: Phorum %(phorum_version)s X-Phorum-%(phorum_mail_code)s-Forum: %(forum_name)s X-Phorum-%(phorum_mail_code)s-Thread: %(thread_id)s X-Phorum-%(phorum_mail_code)s-Parent: %(parent_id)s This message was sent from: %(forum_name)s. <%(phorum_url)s/read.php?f=%(forum_id)s&i=%(msg_id)s&t=%(thread_id)s> ---------------------------------------------------------------- %(msg_body)s ---------------------------------------------------------------- Sent using Papercut version %(__VERSION__)s """ stmt = """ SELECT email_list, email_return FROM forums WHERE LENGTH(email_list) > 0 AND id=%s""" % (forum_id) num_rows = self.cursor.execute(stmt) if num_rows == 1: email_list, email_return = self.cursor.fetchone() msg_body = strutil.wrap(msg_body) if len(msg_email) > 0: msg_email = '<%s>' % msg_email else: msg_email = '' random_msgid = md5.new(str(time.clock())).hexdigest() # this is pretty ugly, right ? from papercut import __VERSION__ phorum_version = settings.phorum_version current_msg = notification_mail_tpl % vars() smtp.sendmail('%s %s' % (msg_author, msg_email), email_list, current_msg) smtp.quit() def get_NEWGROUPS(self, ts, group='%'): # since phorum doesn't record when each forum was created, we have no way of knowing this... return None def get_NEWNEWS(self, ts, group='*'): stmt = """ SELECT nntp_group_name, table_name FROM forums WHERE nntp_group_name='%s' ORDER BY nntp_group_name ASC""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) articles = [] for group, table in result: stmt = """ SELECT id FROM %s WHERE approved='Y' AND UNIX_TIMESTAMP(datestamp) >= %s""" % (table, ts) num_rows = self.cursor.execute(stmt) if num_rows == 0: continue ids = list(self.cursor.fetchall()) for id in ids: articles.append("<%s@%s>" % (id, group)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): table_name = self.get_table_name(group_name) result = self.get_table_stats(table_name) return (result[0], result[2], result[1]) def get_LIST(self, username=""): stmt = """ SELECT nntp_group_name, table_name FROM forums WHERE LENGTH(nntp_group_name) > 0 ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) if len(result) == 0: return "" else: lists = [] for group_name, table in result: total, maximum, minimum = self.get_table_stats(table) if settings.server_type == 'read-only': lists.append("%s %s %s n" % (group_name, maximum, minimum)) else: lists.append("%s %s %s y" % (group_name, maximum, minimum)) return "\r\n".join(lists) def get_STAT(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id=%s""" % (table_name, id) return self.cursor.execute(stmt) def get_ARTICLE(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, author, email, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, body, parent FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id=B.id AND A.id=%s""" % (table_name, table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[6] != 0: headers.append("References: <%s@%s>" % (result[6], group_name)) return ("\r\n".join(headers), strutil.format_body(result[5])) def get_LAST(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id < %s ORDER BY id DESC LIMIT 0, 1""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_NEXT(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id > %s ORDER BY id ASC LIMIT 0, 1""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_HEAD(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id, author, email, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, parent FROM %s WHERE approved='Y' AND id=%s""" % (table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[5] != 0: headers.append("References: <%s@%s>" % (result[5], group_name)) return "\r\n".join(headers) def get_BODY(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT B.body FROM %s A, %s_bodies B WHERE A.id=B.id AND A.approved='Y' AND B.id=%s""" % (table_name, table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None else: return strutil.format_body(self.cursor.fetchone()[0]) def get_XOVER(self, group_name, start_id, end_id='ggg'): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id=B.id AND A.id >= %s""" % (table_name, table_name, start_id) if end_id != 'ggg': stmt = "%s AND A.id <= %s" % (stmt, end_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) overviews = [] for row in result: if row[3] == '': author = row[2] else: author = "%s <%s>" % (row[2], row[3]) formatted_time = strutil.get_formatted_time(time.localtime(row[5])) message_id = "<%s@%s>" % (row[0], group_name) line_count = len(row[6].split('\n')) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, row[0]) if row[1] != 0: reference = "<%s@%s>" % (row[1], group_name) else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (row[0], row[4], author, formatted_time, message_id, reference, len(strutil.format_body(row[6])), line_count, xref)) return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): # XXX: need to actually check for the header values being passed as # XXX: not all header names map to column names on the tables table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND %s REGEXP '%s' AND A.id = B.id AND A.id >= %s""" % (table_name, table_name, header, strutil.format_wildcards(pattern), start_id) if end_id != 'ggg': stmt = "%s AND A.id <= %s" % (stmt, end_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchall()) hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': # XXX: totally broken with empty values for the email address hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def get_LISTGROUP(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' ORDER BY id ASC""" % (table_name) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s" % k for k in result]) def get_XGTITLE(self, pattern=None): stmt = """ SELECT nntp_group_name, description FROM forums WHERE LENGTH(nntp_group_name) > 0""" if pattern != None: stmt = stmt + """ AND nntp_group_name REGEXP '%s'""" % (strutil.format_wildcards(pattern)) stmt = stmt + """ ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s %s" % (k, v) for k, v in result]) def get_XHDR(self, group_name, header, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id = B.id AND """ % (table_name, table_name) if style == 'range': stmt = '%s A.id >= %s' % (stmt, range[0]) if len(range) == 2: stmt = '%s AND A.id <= %s' % (stmt, range[1]) else: stmt = '%s A.id = %s' % (stmt, range[0]) if self.cursor.execute(stmt) == 0: return None result = self.cursor.fetchall() hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, lines, ip_address, username=''): table_name = self.get_table_name(group_name) body = self.get_message_body(lines) author, email = from_regexp.search(lines, 0).groups() subject = subject_regexp.search(lines, 0).groups()[0].strip() # patch by Andreas Wegmann to fix the handling of unusual encodings of messages lines = mime_decode_header(re.sub(q_quote_multiline, "=?\\1?Q?\\2\\3?=", lines)) if lines.find('References') != -1: # get the 'modifystamp' value from the parent (if any) references = references_regexp.search(lines, 0).groups() parent_id, void = references[-1].strip().split('@') stmt = """ SELECT IF(MAX(id) IS NULL, 1, MAX(id)+1) AS next_id FROM %s""" % (table_name) num_rows = self.cursor.execute(stmt) if num_rows == 0: new_id = 1 else: new_id = self.cursor.fetchone()[0] stmt = """ SELECT id, thread, modifystamp FROM %s WHERE approved='Y' AND id=%s GROUP BY id""" % (table_name, parent_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None parent_id, thread_id, modifystamp = self.cursor.fetchone() else: stmt = """ SELECT IF(MAX(id) IS NULL, 1, MAX(id)+1) AS next_id, UNIX_TIMESTAMP() FROM %s""" % (table_name) self.cursor.execute(stmt) new_id, modifystamp = self.cursor.fetchone() parent_id = 0 thread_id = new_id stmt = """ INSERT INTO %s ( id, datestamp, thread, parent, author, subject, email, host, email_reply, approved, msgid, modifystamp, userid ) VALUES ( %s, NOW(), %s, %s, '%s', '%s', '%s', '%s', 'N', 'Y', '', %s, 0 ) """ % (table_name, new_id, thread_id, parent_id, self.quote_string(author.strip()), self.quote_string(subject), self.quote_string(email), ip_address, modifystamp) if not self.cursor.execute(stmt): return None else: # insert into the '*_bodies' table stmt = """ INSERT INTO %s_bodies ( id, body, thread ) VALUES ( %s, '%s', %s )""" % (table_name, new_id, self.quote_string(body), thread_id) if not self.cursor.execute(stmt): # delete from 'table_name' before returning.. stmt = """ DELETE FROM %s WHERE id=%s""" % (table_name, new_id) self.cursor.execute(stmt) return None else: # alert forum moderators self.send_notifications(group_name, new_id, thread_id, parent_id, author.strip(), email, subject, body) return 1 papercut-0.9.13.orig/storage/phorum_mysql_fix.sql0000644000175000017500000000101110327536616021206 0ustar lunarlunar# # Please change the values here as appropriate to your # setup (i.e. table name or size of the 'nntp_group_name' field) # # Warning: Do not change the field name to something else than 'nttp_group_name'! # ALTER TABLE forums ADD nntp_group_name VARCHAR(30) AFTER id; ALTER TABLE forums ADD UNIQUE (nntp_group_name); # # After dumping this file into MySQL you will need to manually update the contents # of the 'nttp_group_name' field to associate a table name / forum with a newsgroup to # be available on Papercut #papercut-0.9.13.orig/storage/phorum_pgsql_fix.sql0000644000175000017500000000014610327536616021177 0ustar lunarlunarALTER TABLE forums ADD nntp_group_name VARCHAR(30); ALTER TABLE forums ADD UNIQUE (nntp_group_name); papercut-0.9.13.orig/storage/phpbb_mysql_fix.sql0000644000175000017500000000103410327536616020774 0ustar lunarlunar# # Please change the values here as appropriate to your # setup (i.e. table name or size of the 'nntp_group_name' field) # # Warning: Do not change the field name to something else than 'nttp_group_name'! # ALTER TABLE phpbb_forums ADD nntp_group_name VARCHAR(30) AFTER forum_name; ALTER TABLE phpbb_forums ADD UNIQUE (nntp_group_name); # # After dumping this file into MySQL you will need to manually update the contents # of the 'nttp_group_name' field to associate a table name / forum with a newsgroup to # be available on Papercut #papercut-0.9.13.orig/storage/phpnuke_phpbb_mysql.py0000644000175000017500000007351410327536616021525 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002, 2003, 2004 Joao Prado Maia. See the LICENSE file for more information. import MySQLdb import time from mimify import mime_encode_header, mime_decode_header import re import settings import md5 import mime import strutil # patch by Andreas Wegmann to fix the handling of unusual encodings of messages q_quote_multiline = re.compile("=\?(.*?)\?[qQ]\?(.*?)\?=.*?=\?\\1\?[qQ]\?(.*?)\?=", re.M | re.S) # we don't need to compile the regexps everytime.. doubleline_regexp = re.compile("^\.\.", re.M) singleline_regexp = re.compile("^\.", re.M) from_regexp = re.compile("^From:(.*)<(.*)>", re.M) subject_regexp = re.compile("^Subject:(.*)", re.M) references_regexp = re.compile("^References:(.*)<(.*)>", re.M) lines_regexp = re.compile("^Lines:(.*)", re.M) class Papercut_Storage: """ Storage Backend interface for the nuke port of phpBB (http://www.phpnuke.org) This is the interface for PHPNuke/NukeBB running on a MySQL database. For more information on the structure of the 'storage' package, please refer to the __init__.py available on the 'storage' sub-directory. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def get_message_body(self, headers): """Parses and returns the most appropriate message body possible. The function tries to extract the plaintext version of a MIME based message, and if it is not available then it returns the html version. """ return mime.get_text_message(headers) def quote_string(self, text): """Quotes strings the MySQL way.""" return text.replace("'", "\\'") def make_bbcode_uid(self): return md5.new(str(time.clock())).hexdigest() def encode_ip(self, dotquad_ip): t = dotquad_ip.split('.') return '%02x%02x%02x%02x' % (int(t[0]), int(t[1]), int(t[2]), int(t[3])) def group_exists(self, group_name): stmt = """ SELECT COUNT(*) AS total FROM %sforums WHERE LOWER(nntp_group_name)=LOWER('%s')""" % (settings.phpbb_table_prefix, group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def article_exists(self, group_name, style, range): forum_id = self.get_forum(group_name) stmt = """ SELECT COUNT(*) AS total FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) if style == 'range': stmt = "%s AND post_id > %s" % (stmt, range[0]) if len(range) == 2: stmt = "%s AND post_id < %s" % (stmt, range[1]) else: stmt = "%s AND post_id = %s" % (stmt, range[0]) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_first_article(self, group_name): forum_id = self.get_forum(group_name) stmt = """ SELECT IF(MIN(post_id) IS NULL, 0, MIN(post_id)) AS first_article FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_group_stats(self, group_name): total, max, min = self.get_forum_stats(self.get_forum(group_name)) return (total, min, max, group_name) def get_forum_stats(self, forum_id): stmt = """ SELECT COUNT(post_id) AS total, IF(MAX(post_id) IS NULL, 0, MAX(post_id)) AS maximum, IF(MIN(post_id) IS NULL, 0, MIN(post_id)) AS minimum FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone() def get_forum(self, group_name): stmt = """ SELECT forum_id FROM %sforums WHERE nntp_group_name='%s'""" % (settings.phpbb_table_prefix, group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_NEWGROUPS(self, ts, group='%'): # since phpBB doesn't record when each forum was created, we have no way of knowing this... return None def get_NEWNEWS(self, ts, group='*'): stmt = """ SELECT nntp_group_name, forum_id FROM %sforums WHERE nntp_group_name LIKE '%s' ORDER BY nntp_group_name ASC""" % (settings.phpbb_table_prefix, group.replace('*', '%')) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) articles = [] for group_name, forum_id in result: stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_time >= %s""" % (settings.phpbb_table_prefix, forum_id, ts) num_rows = self.cursor.execute(stmt) if num_rows == 0: continue ids = list(self.cursor.fetchall()) for id in ids: articles.append("<%s@%s>" % (id, group_name)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): forum_id = self.get_forum(group_name) result = self.get_forum_stats(forum_id) return (result[0], result[2], result[1]) def get_LIST(self, username=""): stmt = """ SELECT nntp_group_name, forum_id FROM %sforums WHERE LENGTH(nntp_group_name) > 0 ORDER BY nntp_group_name ASC""" % (settings.phpbb_table_prefix) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) if len(result) == 0: return "" else: lists = [] for group_name, forum_id in result: total, maximum, minimum = self.get_forum_stats(forum_id) if settings.server_type == 'read-only': lists.append("%s %s %s n" % (group_name, maximum, minimum)) else: lists.append("%s %s %s y" % (group_name, maximum, minimum)) return "\r\n".join(lists) def get_STAT(self, group_name, id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_id=%s""" % (settings.phpbb_table_prefix, forum_id, id) return self.cursor.execute(stmt) def get_ARTICLE(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix stmt = """ SELECT A.post_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', E.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.topic_id, A.post_username, MIN(D.post_id) FROM %sposts A, %sposts_text B INNER JOIN %sposts D ON D.topic_id=A.topic_id INNER JOIN %stopics E ON A.topic_id = E.topic_id LEFT JOIN %susers C ON A.poster_id=C.user_id WHERE A.forum_id=%s AND A.post_id=B.post_id AND A.post_id=%s GROUP BY D.topic_id""" % (prefix, prefix, prefix, prefix, nuke_prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) # check if there is a registered user if result[7] == '': if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) else: author = result[7] formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[8] != result[0]: headers.append("References: <%s@%s>" % (result[8], group_name)) return ("\r\n".join(headers), strutil.format_body(result[5])) def get_LAST(self, group_name, current_id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE post_id < %s AND forum_id=%s ORDER BY post_id DESC LIMIT 0, 1""" % (settings.phpbb_table_prefix, current_id, forum_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_NEXT(self, group_name, current_id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_id > %s ORDER BY post_id ASC LIMIT 0, 1""" % (settings.phpbb_table_prefix, forum_id, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_HEAD(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix stmt = """ SELECT A.post_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', E.topic_title) ELSE B.post_subject END, A.post_time, A.topic_id, A.post_username, MIN(D.post_id) FROM %sposts A, %sposts_text B INNER JOIN %stopics E ON A.topic_id = E.topic_id INNER JOIN %sposts D ON D.topic_id=A.topic_id LEFT JOIN %susers C ON A.poster_id=C.user_id WHERE A.forum_id=%s AND A.post_id=B.post_id AND A.post_id=%s GROUP BY D.topic_id""" % (prefix, prefix, prefix, prefix, nuke_prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) # check if there is a registered user if len(result[6]) == 0 or result[6] == '': if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) else: author = result[6] formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[7] != result[0]: headers.append("References: <%s@%s>" % (result[7], group_name)) return "\r\n".join(headers) def get_BODY(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT B.post_text FROM %sposts A, %sposts_text B WHERE A.post_id=B.post_id AND A.forum_id=%s AND A.post_id=%s""" % (prefix, prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None else: return strutil.format_body(self.cursor.fetchone()[0]) def get_XOVER(self, group_name, start_id, end_id='ggg'): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix stmt = """ SELECT A.post_id, A.topic_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', D.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %susers C ON A.poster_id=C.user_id LEFT JOIN %stopics D ON A.topic_id = D.topic_id WHERE A.post_id=B.post_id AND A.forum_id=%s AND A.post_id >= %s""" % (prefix, prefix, nuke_prefix, prefix, forum_id, start_id) if end_id != 'ggg': stmt = "%s AND A.post_id <= %s" % (stmt, end_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) overviews = [] for row in result: if row[7] == '': if row[3] == '': author = row[2] else: author = "%s <%s>" % (row[2], row[3]) else: author = row[7] formatted_time = strutil.get_formatted_time(time.localtime(row[5])) message_id = "<%s@%s>" % (row[0], group_name) line_count = len(row[6].split('\n')) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, row[0]) if row[1] != row[0]: reference = "<%s@%s>" % (row[1], group_name) else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (row[0], row[4], author, formatted_time, message_id, reference, len(strutil.format_body(row[6])), line_count, xref)) return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): # XXX: need to actually check for the header values being passed as # XXX: not all header names map to column names on the tables forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix stmt = """ SELECT A.post_id, A.topic_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', D.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %susers C ON A.poster_id=C.user_id LEFT JOIN %stopics D ON A.topic_id = D.topic_id WHERE A.forum_id=%s AND %s REGEXP '%s' AND A.post_id = B.post_id AND A.post_id >= %s""" % (prefix, prefix, nuke_prefix, prefix, forum_id, header, strutil.format_wildcards(pattern), start_id) if end_id != 'ggg': stmt = "%s AND A.post_id <= %s" % (stmt, end_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchall()) hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': # XXX: totally broken with empty values for the email address hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def get_LISTGROUP(self, group_name): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s ORDER BY post_id ASC""" % (settings.phpbb_table_prefix, forum_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s" % k for k in result]) def get_XGTITLE(self, pattern=None): stmt = """ SELECT nntp_group_name, forum_desc FROM %sforums WHERE LENGTH(nntp_group_name) > 0""" % (settings.phpbb_table_prefix) if pattern != None: stmt = stmt + """ AND nntp_group_name REGEXP '%s'""" % (strutil.format_wildcards(pattern)) stmt = stmt + """ ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s %s" % (k, v) for k, v in result]) def get_XHDR(self, group_name, header, style, range): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix stmt = """ SELECT A.post_id, A.topic_id, D.username, D.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', C.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %stopics C ON A.topic_id = C.topic_id LEFT JOIN %susers D ON A.poster_id=D.user_id WHERE A.forum_id=%s AND A.post_id = B.post_id AND """ % (prefix, prefix, prefix, nuke_prefix, forum_id) if style == 'range': stmt = '%s A.post_id >= %s' % (stmt, range[0]) if len(range) == 2: stmt = '%s AND A.post_id <= %s' % (stmt, range[1]) else: stmt = '%s A.post_id = %s' % (stmt, range[0]) if self.cursor.execute(stmt) == 0: return None result = self.cursor.fetchall() hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, lines, ip_address, username=''): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix nuke_prefix = settings.nuke_table_prefix # patch by Andreas Wegmann to fix the handling of unusual encodings of messages lines = mime_decode_header(re.sub(q_quote_multiline, "=?\\1?Q?\\2\\3?=", lines)) body = self.get_message_body(lines) author, email = from_regexp.search(lines, 0).groups() subject = subject_regexp.search(lines, 0).groups()[0].strip() # get the authentication information now if username != '': stmt = """ SELECT user_id FROM %susers WHERE username='%s'""" % (nuke_prefix, username) num_rows = self.cursor.execute(stmt) if num_rows == 0: poster_id = -1 else: poster_id = self.cursor.fetchone()[0] post_username = '' else: poster_id = -1 post_username = author if lines.find('References') != -1: # get the 'modifystamp' value from the parent (if any) references = references_regexp.search(lines, 0).groups() parent_id, void = references[-1].strip().split('@') stmt = """ SELECT topic_id FROM %sposts WHERE post_id=%s GROUP BY post_id""" % (prefix, parent_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None thread_id = self.cursor.fetchone()[0] else: # create a new topic stmt = """ INSERT INTO %stopics ( forum_id, topic_title, topic_poster, topic_time, topic_status, topic_vote, topic_type ) VALUES ( %s, '%s', %s, UNIX_TIMESTAMP(), 0, 0, 0 )""" % (prefix, forum_id, self.quote_string(subject), poster_id) self.cursor.execute(stmt) thread_id = self.cursor.insert_id() stmt = """ INSERT INTO %sposts ( topic_id, forum_id, poster_id, post_time, poster_ip, post_username, enable_bbcode, enable_html, enable_smilies, enable_sig ) VALUES ( %s, %s, %s, UNIX_TIMESTAMP(), '%s', '%s', 1, 0, 1, 0 )""" % (prefix, thread_id, forum_id, poster_id, self.encode_ip(ip_address), post_username) self.cursor.execute(stmt) new_id = self.cursor.insert_id() if not new_id: return None else: # insert into the '*posts_text' table stmt = """ INSERT INTO %sposts_text ( post_id, bbcode_uid, post_subject, post_text ) VALUES ( %s, '%s', '%s', '%s' )""" % (prefix, new_id, self.make_bbcode_uid(), self.quote_string(subject), self.quote_string(body)) if not self.cursor.execute(stmt): # delete from 'topics' and 'posts' tables before returning... stmt = """ DELETE FROM %stopics WHERE topic_id=%s""" % (prefix, thread_id) self.cursor.execute(stmt) stmt = """ DELETE FROM %sposts WHERE post_id=%s""" % (prefix, new_id) self.cursor.execute(stmt) return None else: if lines.find('References') != -1: # update the total number of posts in the forum stmt = """ UPDATE %sforums SET forum_posts=forum_posts+1, forum_last_post_id=%s WHERE forum_id=%s """ % (settings.phpbb_table_prefix, new_id, forum_id) self.cursor.execute(stmt) else: # update the total number of topics and posts in the forum stmt = """ UPDATE %sforums SET forum_topics=forum_topics+1, forum_posts=forum_posts+1, forum_last_post_id=%s WHERE forum_id=%s """ % (settings.phpbb_table_prefix, new_id, forum_id) self.cursor.execute(stmt) # update the user's post count, if this is indeed a real user if poster_id != -1: stmt = """ UPDATE %susers SET user_posts=user_posts+1 WHERE user_id=%s""" % (nuke_prefix, poster_id) self.cursor.execute(stmt) # setup last post on the topic thread (Patricio Anguita ) stmt = """ UPDATE %stopics SET topic_replies=topic_replies+1, topic_last_post_id=%s WHERE topic_id=%s""" % (prefix, new_id, thread_id) self.cursor.execute(stmt) # if this is the first post on the thread.. (Patricio Anguita ) if lines.find('References') == -1: stmt = """ UPDATE %stopics SET topic_first_post_id=%s WHERE topic_id=%s AND topic_first_post_id=0""" % (prefix, new_id, thread_id) self.cursor.execute(stmt) return 1 papercut-0.9.13.orig/storage/phpbb_mysql.py0000644000175000017500000007522710327536616017776 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002, 2003, 2004 Joao Prado Maia. See the LICENSE file for more information. # $Id: phpbb_mysql.py,v 1.20 2004/08/01 01:51:48 jpm Exp $ import MySQLdb import time from mimify import mime_encode_header, mime_decode_header import re import settings import md5 import mime import strutil # patch by Andreas Wegmann to fix the handling of unusual encodings of messages q_quote_multiline = re.compile("=\?(.*?)\?[qQ]\?(.*?)\?=.*?=\?\\1\?[qQ]\?(.*?)\?=", re.M | re.S) # we don't need to compile the regexps everytime.. doubleline_regexp = re.compile("^\.\.", re.M) singleline_regexp = re.compile("^\.", re.M) from_regexp = re.compile("^From:(.*)<(.*)>", re.M) subject_regexp = re.compile("^Subject:(.*)", re.M) references_regexp = re.compile("^References:(.*)<(.*)>", re.M) lines_regexp = re.compile("^Lines:(.*)", re.M) class Papercut_Storage: """ Storage Backend interface for the Phorum web message board software (http://phorum.org) This is the interface for Phorum running on a MySQL database. For more information on the structure of the 'storage' package, please refer to the __init__.py available on the 'storage' sub-directory. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def get_message_body(self, headers): """Parses and returns the most appropriate message body possible. The function tries to extract the plaintext version of a MIME based message, and if it is not available then it returns the html version. """ return mime.get_text_message(headers) def quote_string(self, text): """Quotes strings the MySQL way.""" return text.replace("'", "\\'") def make_bbcode_uid(self): return md5.new(str(time.clock())).hexdigest() def encode_ip(self, dotquad_ip): t = dotquad_ip.split('.') return '%02x%02x%02x%02x' % (int(t[0]), int(t[1]), int(t[2]), int(t[3])) def group_exists(self, group_name): stmt = """ SELECT COUNT(*) AS total FROM %sforums WHERE LOWER(nntp_group_name)=LOWER('%s')""" % (settings.phpbb_table_prefix, group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def article_exists(self, group_name, style, range): forum_id = self.get_forum(group_name) stmt = """ SELECT COUNT(*) AS total FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) if style == 'range': stmt = "%s AND post_id > %s" % (stmt, range[0]) if len(range) == 2: stmt = "%s AND post_id < %s" % (stmt, range[1]) else: stmt = "%s AND post_id = %s" % (stmt, range[0]) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_first_article(self, group_name): forum_id = self.get_forum(group_name) stmt = """ SELECT IF(MIN(post_id) IS NULL, 0, MIN(post_id)) AS first_article FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_group_stats(self, group_name): total, max, min = self.get_forum_stats(self.get_forum(group_name)) return (total, min, max, group_name) def get_forum_stats(self, forum_id): stmt = """ SELECT COUNT(post_id) AS total, IF(MAX(post_id) IS NULL, 0, MAX(post_id)) AS maximum, IF(MIN(post_id) IS NULL, 0, MIN(post_id)) AS minimum FROM %sposts WHERE forum_id=%s""" % (settings.phpbb_table_prefix, forum_id) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone() def get_forum(self, group_name): stmt = """ SELECT forum_id FROM %sforums WHERE nntp_group_name='%s'""" % (settings.phpbb_table_prefix, group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_NEWGROUPS(self, ts, group='%'): # since phpBB doesn't record when each forum was created, we have no way of knowing this... return None def get_NEWNEWS(self, ts, group='*'): stmt = """ SELECT nntp_group_name, forum_id FROM %sforums WHERE nntp_group_name LIKE '%s' ORDER BY nntp_group_name ASC""" % (settings.phpbb_table_prefix, group.replace('*', '%')) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) articles = [] for group_name, forum_id in result: stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_time >= %s""" % (settings.phpbb_table_prefix, forum_id, ts) num_rows = self.cursor.execute(stmt) if num_rows == 0: continue ids = list(self.cursor.fetchall()) for id in ids: articles.append("<%s@%s>" % (id, group_name)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): forum_id = self.get_forum(group_name) result = self.get_forum_stats(forum_id) return (result[0], result[2], result[1]) def get_LIST(self, username=""): # If the username is supplied, then find what he is allowed to see if len(username) > 0: stmt = """ SELECT DISTINCT f.nntp_group_name, f.forum_id FROM %sforums AS f INNER JOIN %sauth_access AS aa ON f.forum_id=aa.forum_id INNER JOIN %suser_group AS ug ON aa.group_id=ug.group_id INNER JOIN %susers AS u ON ug.user_id=u.user_id WHERE u.username='%s' AND LENGTH(f.nntp_group_name) > 0 OR f.auth_view = 0""" % (settings.phpbb_table_prefix, settings.phpbb_table_prefix, settings.phpbb_table_prefix, settings.phpbb_table_prefix, username) else: stmt = """ SELECT nntp_group_name, forum_id FROM %sforums WHERE LENGTH(nntp_group_name) > 0 AND auth_view=0 ORDER BY nntp_group_name ASC""" % (settings.phpbb_table_prefix) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) if len(result) == 0: return "" else: lists = [] for group_name, forum_id in result: total, maximum, minimum = self.get_forum_stats(forum_id) if settings.server_type == 'read-only': lists.append("%s %s %s n" % (group_name, maximum, minimum)) else: lists.append("%s %s %s y" % (group_name, maximum, minimum)) return "\r\n".join(lists) def get_STAT(self, group_name, id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_id=%s""" % (settings.phpbb_table_prefix, forum_id, id) return self.cursor.execute(stmt) def get_ARTICLE(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT A.post_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', E.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.topic_id, A.post_username, MIN(D.post_id) FROM %sposts A, %sposts_text B INNER JOIN %sposts D ON D.topic_id=A.topic_id INNER JOIN %stopics E ON A.topic_id = E.topic_id LEFT JOIN %susers C ON A.poster_id=C.user_id WHERE A.forum_id=%s AND A.post_id=B.post_id AND A.post_id=%s GROUP BY D.topic_id""" % (prefix, prefix, prefix, prefix, prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) # check if there is a registered user if result[7] == '': if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) else: author = result[7] formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[8] != result[0]: headers.append("References: <%s@%s>" % (result[8], group_name)) return ("\r\n".join(headers), strutil.format_body(result[5])) def get_LAST(self, group_name, current_id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE post_id < %s AND forum_id=%s ORDER BY post_id DESC LIMIT 0, 1""" % (settings.phpbb_table_prefix, current_id, forum_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_NEXT(self, group_name, current_id): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s AND post_id > %s ORDER BY post_id ASC LIMIT 0, 1""" % (settings.phpbb_table_prefix, forum_id, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_HEAD(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT A.post_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', E.topic_title) ELSE B.post_subject END, A.post_time, A.topic_id, A.post_username, MIN(D.post_id) FROM %sposts A, %sposts_text B INNER JOIN %stopics E ON A.topic_id = E.topic_id INNER JOIN %sposts D ON D.topic_id=A.topic_id LEFT JOIN %susers C ON A.poster_id=C.user_id WHERE A.forum_id=%s AND A.post_id=B.post_id AND A.post_id=%s GROUP BY D.topic_id""" % (prefix, prefix, prefix, prefix, prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) # check if there is a registered user if len(result[6]) == 0 or result[6] == '': if len(result[2]) == 0: author = result[1] else: author = "%s <%s>" % (result[1], result[2]) else: author = result[6] formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[7] != result[0]: headers.append("References: <%s@%s>" % (result[7], group_name)) return "\r\n".join(headers) def get_BODY(self, group_name, id): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT B.post_text FROM %sposts A, %sposts_text B WHERE A.post_id=B.post_id AND A.forum_id=%s AND A.post_id=%s""" % (prefix, prefix, forum_id, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None else: return strutil.format_body(self.cursor.fetchone()[0]) def get_XOVER(self, group_name, start_id, end_id='ggg'): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT A.post_id, A.topic_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', D.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %susers C ON A.poster_id=C.user_id LEFT JOIN %stopics D ON A.topic_id = D.topic_id WHERE A.post_id=B.post_id AND A.forum_id=%s AND A.post_id >= %s""" % (prefix, prefix, prefix, prefix, forum_id, start_id) if end_id != 'ggg': stmt = "%s AND A.post_id <= %s" % (stmt, end_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) overviews = [] for row in result: if row[7] == '': if row[3] == '': author = row[2] else: author = "%s <%s>" % (row[2], row[3]) else: author = row[7] formatted_time = strutil.get_formatted_time(time.localtime(row[5])) message_id = "<%s@%s>" % (row[0], group_name) line_count = len(row[6].split('\n')) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, row[0]) if row[1] != row[0]: reference = "<%s@%s>" % (row[1], group_name) else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (row[0], row[4], author, formatted_time, message_id, reference, len(strutil.format_body(row[6])), line_count, xref)) return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): # XXX: need to actually check for the header values being passed as # XXX: not all header names map to column names on the tables forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT A.post_id, A.topic_id, C.username, C.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', D.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %susers C ON A.poster_id=C.user_id LEFT JOIN %stopics D ON A.topic_id = D.topic_id WHERE A.forum_id=%s AND %s REGEXP '%s' AND A.post_id = B.post_id AND A.post_id >= %s""" % (prefix, prefix, prefix, prefix, forum_id, header, strutil.format_wildcards(pattern), start_id) if end_id != 'ggg': stmt = "%s AND A.post_id <= %s" % (stmt, end_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchall()) hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': # XXX: totally broken with empty values for the email address hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def get_LISTGROUP(self, group_name): forum_id = self.get_forum(group_name) stmt = """ SELECT post_id FROM %sposts WHERE forum_id=%s ORDER BY post_id ASC""" % (settings.phpbb_table_prefix, forum_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s" % k for k in result]) def get_XGTITLE(self, pattern=None): stmt = """ SELECT nntp_group_name, forum_desc FROM %sforums WHERE LENGTH(nntp_group_name) > 0""" % (settings.phpbb_table_prefix) if pattern != None: stmt = stmt + """ AND nntp_group_name REGEXP '%s'""" % (strutil.format_wildcards(pattern)) stmt = stmt + """ ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s %s" % (k, v) for k, v in result]) def get_XHDR(self, group_name, header, style, range): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix stmt = """ SELECT A.post_id, A.topic_id, D.username, D.user_email, CASE WHEN B.post_subject = '' THEN CONCAT('Re: ', C.topic_title) ELSE B.post_subject END, A.post_time, B.post_text, A.post_username FROM %sposts A, %sposts_text B LEFT JOIN %stopics C ON A.topic_id = C.topic_id LEFT JOIN %susers D ON A.poster_id=D.user_id WHERE A.forum_id=%s AND A.post_id = B.post_id AND """ % (prefix, prefix, prefix, prefix, forum_id) if style == 'range': stmt = '%s A.post_id >= %s' % (stmt, range[0]) if len(range) == 2: stmt = '%s AND A.post_id <= %s' % (stmt, range[1]) else: stmt = '%s A.post_id = %s' % (stmt, range[0]) if self.cursor.execute(stmt) == 0: return None result = self.cursor.fetchall() hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4])) elif header.upper() == 'FROM': hdrs.append('%s %s <%s>' % (row[0], row[2], row[3])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, lines, ip_address, username=''): forum_id = self.get_forum(group_name) prefix = settings.phpbb_table_prefix # patch by Andreas Wegmann to fix the handling of unusual encodings of messages lines = mime_decode_header(re.sub(q_quote_multiline, "=?\\1?Q?\\2\\3?=", lines)) body = self.get_message_body(lines) author, email = from_regexp.search(lines, 0).groups() subject = subject_regexp.search(lines, 0).groups()[0].strip() # get the authentication information now if username != '': stmt = """ SELECT user_id FROM %susers WHERE username='%s'""" % (prefix, username) num_rows = self.cursor.execute(stmt) if num_rows == 0: poster_id = -1 else: poster_id = self.cursor.fetchone()[0] post_username = '' else: poster_id = -1 post_username = author if lines.find('References') != -1: # get the 'modifystamp' value from the parent (if any) references = references_regexp.search(lines, 0).groups() parent_id, void = references[-1].strip().split('@') stmt = """ SELECT topic_id FROM %sposts WHERE post_id=%s GROUP BY post_id""" % (prefix, parent_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None thread_id = self.cursor.fetchone()[0] else: # create a new topic stmt = """ INSERT INTO %stopics ( forum_id, topic_title, topic_poster, topic_time, topic_status, topic_vote, topic_type ) VALUES ( %s, '%s', %s, UNIX_TIMESTAMP(), 0, 0, 0 )""" % (prefix, forum_id, self.quote_string(subject), poster_id) self.cursor.execute(stmt) thread_id = self.cursor.insert_id() stmt = """ INSERT INTO %sposts ( topic_id, forum_id, poster_id, post_time, poster_ip, post_username, enable_bbcode, enable_html, enable_smilies, enable_sig ) VALUES ( %s, %s, %s, UNIX_TIMESTAMP(), '%s', '%s', 1, 0, 1, 0 )""" % (prefix, thread_id, forum_id, poster_id, self.encode_ip(ip_address), post_username) self.cursor.execute(stmt) new_id = self.cursor.insert_id() if not new_id: return None else: # insert into the '*posts_text' table stmt = """ INSERT INTO %sposts_text ( post_id, bbcode_uid, post_subject, post_text ) VALUES ( %s, '%s', '%s', '%s' )""" % (prefix, new_id, self.make_bbcode_uid(), self.quote_string(subject), self.quote_string(body)) if not self.cursor.execute(stmt): # delete from 'topics' and 'posts' tables before returning... stmt = """ DELETE FROM %stopics WHERE topic_id=%s""" % (prefix, thread_id) self.cursor.execute(stmt) stmt = """ DELETE FROM %sposts WHERE post_id=%s""" % (prefix, new_id) self.cursor.execute(stmt) return None else: if lines.find('References') != -1: # update the total number of posts in the forum stmt = """ UPDATE %sforums SET forum_posts=forum_posts+1, forum_last_post_id=%s WHERE forum_id=%s """ % (settings.phpbb_table_prefix, new_id, forum_id) self.cursor.execute(stmt) else: # update the total number of topics and posts in the forum stmt = """ UPDATE %sforums SET forum_topics=forum_topics+1, forum_posts=forum_posts+1, forum_last_post_id=%s WHERE forum_id=%s """ % (settings.phpbb_table_prefix, new_id, forum_id) self.cursor.execute(stmt) # update the user's post count, if this is indeed a real user if poster_id != -1: stmt = """ UPDATE %susers SET user_posts=user_posts+1 WHERE user_id=%s""" % (prefix, poster_id) self.cursor.execute(stmt) # setup last post on the topic thread (Patricio Anguita ) stmt = """ UPDATE %stopics SET topic_replies=topic_replies+1, topic_last_post_id=%s WHERE topic_id=%s""" % (prefix, new_id, thread_id) self.cursor.execute(stmt) # if this is the first post on the thread.. (Patricio Anguita ) if lines.find('References') == -1: stmt = """ UPDATE %stopics SET topic_first_post_id=%s WHERE topic_id=%s AND topic_first_post_id=0""" % (prefix, new_id, thread_id) self.cursor.execute(stmt) return 1 papercut-0.9.13.orig/storage/mysql.py0000644000175000017500000004515110327536616016614 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: mysql.py,v 1.44 2004/08/01 01:51:48 jpm Exp $ import MySQLdb import time import re import settings import strutil import mime # we don't need to compile the regexps everytime.. singleline_regexp = re.compile("^\.", re.M) from_regexp = re.compile("^From:(.*)", re.M) subject_regexp = re.compile("^Subject:(.*)", re.M) references_regexp = re.compile("^References:(.*)<(.*)>", re.M) class Papercut_Storage: """ Storage Backend interface for saving the article information in a MySQL database. This is not a storage to implement a web board -> nntp gateway, but a standalone nntp server. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def quote_string(self, text): """Quotes strings the MySQL way.""" return text.replace("'", "\\'") def get_body(self, lines): pass def get_header(self, lines): pass def group_exists(self, group_name): stmt = """ SELECT COUNT(*) AS check FROM papercut_groups WHERE LOWER(name)=LOWER('%s')""" % (group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def article_exists(self, group_name, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT COUNT(*) AS check FROM %s WHERE """ % (table_name) if style == 'range': stmt = "%s id > %s" % (stmt, range[0]) if len(range) == 2: stmt = "%s AND id < %s" % (stmt, range[1]) else: stmt = "%s id = %s" % (stmt, range[0]) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_first_article(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT IF(MIN(id) IS NULL, 0, MIN(id)) AS first_article FROM %s""" % (table_name) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_group_stats(self, group_name): total, max, min = self.get_table_stats(self.get_table_name(group_name)) return (total, min, max, group_name) def get_table_stats(self, table_name): stmt = """ SELECT COUNT(id) AS total, IF(MAX(id) IS NULL, 0, MAX(id)) AS maximum, IF(MIN(id) IS NULL, 0, MIN(id)) AS minimum FROM %s""" % (table_name) num_rows = self.cursor.execute(stmt) return self.cursor.fetchone() def get_table_name(self, group_name): stmt = """ SELECT table_name FROM papercut_groups WHERE name='%s'""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_NEWGROUPS(self, ts, group='%'): return None def get_NEWNEWS(self, ts, group='*'): stmt = """ SELECT name, table_name FROM papercut_groups WHERE name='%s' ORDER BY name ASC""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) articles = [] for group, table in result: stmt = """ SELECT id FROM %s WHERE UNIX_TIMESTAMP(datestamp) >= %s""" % (table, ts) num_rows = self.cursor.execute(stmt) if num_rows == 0: continue ids = list(self.cursor.fetchall()) for id in ids: articles.append("<%s@%s>" % (id, group)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): table_name = self.get_table_name(group_name) result = self.get_table_stats(table_name) return (result[0], result[2], result[1]) def get_LIST(self, username=""): stmt = """ SELECT name, table_name FROM papercut_groups WHERE LENGTH(name) > 0 ORDER BY name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) if len(result) == 0: return "" else: lists = [] for group_name, table in result: total, maximum, minimum = self.get_table_stats(table) if settings.server_type == 'read-only': lists.append("%s %s %s n" % (group_name, maximum, minimum)) else: lists.append("%s %s %s y" % (group_name, maximum, minimum)) return "\r\n".join(lists) def get_STAT(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE id=%s""" % (table_name, id) return self.cursor.execute(stmt) def get_ARTICLE(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id, author, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, body, parent FROM %s WHERE id=%s""" % (table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (result[1])) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (strutil.get_formatted_time(time.localtime(result[3])))) headers.append("Subject: %s" % (result[2])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[5] != 0: headers.append("References: <%s@%s>" % (result[5], group_name)) return ("\r\n".join(headers), strutil.format_body(result[4])) def get_LAST(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE id < %s ORDER BY id DESC LIMIT 0, 1""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_NEXT(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE id > %s ORDER BY id ASC LIMIT 0, 1""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_HEAD(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id, author, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, parent FROM %s WHERE id=%s""" % (table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (result[1])) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (strutil.get_formatted_time(time.localtime(result[3])))) headers.append("Subject: %s" % (result[2])) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[4] != 0: headers.append("References: <%s@%s>" % (result[4], group_name)) return "\r\n".join(headers) def get_BODY(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT body FROM %s WHERE id=%s""" % (table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None else: return strutil.format_body(self.cursor.fetchone()[0]) def get_XOVER(self, group_name, start_id, end_id='ggg'): table_name = self.get_table_name(group_name) stmt = """ SELECT id, parent, author, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, body, line_num, bytes FROM %s WHERE id >= %s""" % (table_name, start_id) if end_id != 'ggg': stmt = "%s AND id <= %s" % (stmt, end_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) overviews = [] for row in result: message_id = "<%s@%s>" % (row[0], group_name) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, row[0]) if row[1] != 0: reference = "<%s@%s>" % (row[1], group_name) else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (row[0], row[3], row[2], strutil.get_formatted_time(time.localtime(row[4])), message_id, reference, row[7], row[6], xref)) return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): table_name = self.get_table_name(group_name) stmt = """ SELECT id, parent, author, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, bytes, line_num FROM %s WHERE id >= %s AND""" % (table_name, header, strutil.format_wildcards(pattern), start_id) if header.upper() == 'SUBJECT': stmt = "%s AND subject REGEXP '%s'" % (stmt, strutil.format_wildcards(pattern)) elif header.upper() == 'FROM': stmt = "%s AND (author REGEXP '%s' OR email REGEXP '%s')" % (stmt, strutil.format_wildcards(pattern), strutil.format_wildcards(pattern)) elif header.upper() == 'DATE': stmt = "%s AND %s" % (stmt, pattern) if end_id != 'ggg': stmt = "%s AND id <= %s" % (stmt, end_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchall()) hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[3])) elif header.upper() == 'FROM': hdrs.append('%s %s' % (row[0], row[2])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[4])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], row[5])) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], row[6])) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def get_LISTGROUP(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s ORDER BY id ASC""" % (table_name) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s" % k for k in result]) def get_XGTITLE(self, pattern=None): stmt = """ SELECT name, description FROM papercut_groups WHERE LENGTH(name) > 0""" if pattern != None: stmt = stmt + """ AND name REGEXP '%s'""" % (strutil.format_wildcards(pattern)) stmt = stmt + """ ORDER BY name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s %s" % (k, v) for k, v in result]) def get_XHDR(self, group_name, header, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT id, parent, author, subject, UNIX_TIMESTAMP(datestamp) AS datestamp, bytes, line_num FROM %s WHERE """ % (table_name) if style == 'range': stmt = '%s id >= %s' % (stmt, range[0]) if len(range) == 2: stmt = '%s AND id <= %s' % (stmt, range[1]) else: stmt = '%s id = %s' % (stmt, range[0]) if self.cursor.execute(stmt) == 0: return None result = self.cursor.fetchall() hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[3])) elif header.upper() == 'FROM': hdrs.append('%s %s' % (row[0], row[2])) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[4])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], row[6])) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], row[7])) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, body, ip_address, username=''): table_name = self.get_table_name(group_name) author = from_regexp.search(body, 0).groups()[0].strip() subject = subject_regexp.search(body, 0).groups()[0].strip() if body.find('References') != -1: references = references_regexp.search(body, 0).groups() parent_id, void = references[-1].strip().split('@') stmt = """ SELECT IF(MAX(id) IS NULL, 1, MAX(id)+1) AS next_id FROM %s""" % (table_name) num_rows = self.cursor.execute(stmt) if num_rows == 0: new_id = 1 else: new_id = self.cursor.fetchone()[0] stmt = """ SELECT id, thread FROM %s WHERE id=%s GROUP BY id""" % (table_name, parent_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None parent_id, thread_id = self.cursor.fetchone() else: stmt = """ SELECT IF(MAX(id) IS NULL, 1, MAX(id)+1) AS next_id FROM %s""" % (table_name) self.cursor.execute(stmt) new_id = self.cursor.fetchone()[0] parent_id = 0 thread_id = new_id body = mime.get_body(body) stmt = """ INSERT INTO %s ( id, datestamp, thread, parent, author, subject, host, body, bytes, line_num ) VALUES ( %s, NOW(), %s, %s, '%s', '%s', '%s', '%s', %s, %s ) """ % (table_name, new_id, thread_id, parent_id, self.quote_string(author), self.quote_string(subject), ip_address, self.quote_string(body), len(body), len(body.split('\n'))) if not self.cursor.execute(stmt): return None else: return 1 papercut-0.9.13.orig/storage/p2p.py0000644000175000017500000000074610327536616016151 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: p2p.py,v 1.2 2002/04/03 23:07:22 jpm Exp $ import settings import anydbm class Papercut_Storage: """ Experimental Backend interface to implement the ideas brainstormed on the following page: http://webseitz.fluxent.com/wiki/PaperCut """ def __init__(self): # check for the p2p directories and dbm file now db = anydbm.open("p2p.dbm", "c") papercut-0.9.13.orig/storage/strutil.py0000644000175000017500000000410010327536616017142 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: strutil.py,v 1.3 2003/02/22 00:46:18 jpm Exp $ import time import re singleline_regexp = re.compile("^\.", re.M) def wrap(text, width=78): """Wraps text at a specified width. This is used on the PhorumMail feature, as to emulate completely the current Phorum behavior when it sends out copies of the posted articles. """ i = 0 while i < len(text): if i + width + 1 > len(text): i = len(text) else: findnl = text.find('\n', i) findspc = text.rfind(' ', i, i+width+1) if findspc != -1: if findnl != -1 and findnl < findspc: i = findnl + 1 else: text = text[:findspc] + '\n' + text[findspc+1:] i = findspc + 1 else: findspc = text.find(' ', i) if findspc != -1: text = text[:findspc] + '\n' + text[findspc+1:] i = findspc + 1 return text def get_formatted_time(time_tuple): """Formats the time tuple in a NNTP friendly way. Some newsreaders didn't like the date format being sent using leading zeros on the days, so we needed to hack our own little format. """ # days without leading zeros, please day = int(time.strftime('%d', time_tuple)) tmp1 = time.strftime('%a,', time_tuple) tmp2 = time.strftime('%b %Y %H:%M:%S %Z', time_tuple) return "%s %s %s" % (tmp1, day, tmp2) def format_body(text): """Formats the body of message being sent to the client. Since the NNTP protocol uses a single dot on a line to denote the end of the response, we need to substitute all leading dots on the body of the message with two dots. """ return singleline_regexp.sub("..", text) def format_wildcards(pattern): return pattern.replace('*', '.*').replace('?', '.*') def format_wildcards_sql(pattern): return pattern.replace('*', '%').replace('?', '%') papercut-0.9.13.orig/storage/__init__.py0000644000175000017500000000312410327536616017200 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: __init__.py,v 1.4 2002/03/26 22:55:00 jpm Exp $ # # Papercut is a pretty dumb (some people might call it smart) server, because it # doesn't know or care where or how the Usenet articles are stored. The system # uses the concept of 'backends' to have access to the data being served by the # Usenet frontend. # # The 'Backends' of Papercut are the actual containers of the Usenet articles, # wherever they might be stored. The initial and proof of concept backend is # the Phorum (http://phorum.org) one, where the Usenet articles are actually # Phorum messages. # # If you want to create a new backend, please use the phorum_mysql.py file as # a guide for the implementation. You will need a lot of reading to understand # the NNTP protocol (i.e. how the NNTP responses should be sent back to the # user), so look under the 'docs' directory for the RFC documents. # # As a side note, Papercut is designed to be a simple as possible, so the actual # formatting of the responses are usually done on the backend itself. This is # for a reason - if Papercut had to format the information coming from the # backends unchanged, it would need to know 'too much', like the inner workings # of the MySQLdb module on the case of the Phorum backend and so on. # # Instead, Papercut expects a formatted return value from most (if not all) # methods of the backend module. This way we can abstract as much as possible # the data format of the articles, and have the main server code as simple and # fast as possible. #papercut-0.9.13.orig/storage/phorum_pgsql.py0000644000175000017500000007030410327536616020165 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: phorum_pgsql.py,v 1.13 2004/08/01 01:51:48 jpm Exp $ from pyPgSQL import PgSQL import time from mimify import mime_encode_header, mime_decode_header import re import settings import mime import strutil import smtplib import md5 # patch by Andreas Wegmann to fix the handling of unusual encodings of messages q_quote_multiline = re.compile("=\?(.*?)\?[qQ]\?(.*?)\?=.*?=\?\\1\?[qQ]\?(.*?)\?=", re.M | re.S) # we don't need to compile the regexps everytime.. doubleline_regexp = re.compile("^\.\.", re.M) singleline_regexp = re.compile("^\.", re.M) from_regexp = re.compile("^From:(.*)<(.*)>", re.M) subject_regexp = re.compile("^Subject:(.*)", re.M) references_regexp = re.compile("^References:(.*)<(.*)>", re.M) lines_regexp = re.compile("^Lines:(.*)", re.M) # phorum configuration files related regexps moderator_regexp = re.compile("(.*)PHORUM\['ForumModeration'\](.*)='(.*)';", re.M) url_regexp = re.compile("(.*)PHORUM\['forum_url'\](.*)='(.*)';", re.M) admin_regexp = re.compile("(.*)PHORUM\['admin_url'\](.*)='(.*)';", re.M) server_regexp = re.compile("(.*)PHORUM\['forum_url'\](.*)='(.*)http://(.*)/(.*)';", re.M) mail_code_regexp = re.compile("(.*)PHORUM\['PhorumMailCode'\](.*)=(.*)'(.*)';", re.M) class Papercut_Storage: """ Storage Backend interface for the Phorum web message board software (http://phorum.org) This is the interface for Phorum running on a PostgreSQL database. For more information on the structure of the 'storage' package, please refer to the __init__.py available on the 'storage' sub-directory. """ def __init__(self): self.conn = PgSQL.connect(host=settings.dbhost, database=settings.dbname, user=settings.dbuser, password=settings.dbpass) self.cursor = self.conn.cursor() def get_message_body(self, headers): """Parses and returns the most appropriate message body possible. The function tries to extract the plaintext version of a MIME based message, and if it is not available then it returns the html version. """ return mime.get_text_message(headers) def group_exists(self, group_name): stmt = """ SELECT COUNT(*) AS total FROM forums WHERE LOWER(nntp_group_name)=LOWER('%s')""" % (group_name) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def article_exists(self, group_name, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT COUNT(*) AS total FROM %s WHERE approved='Y'""" % (table_name) if style == 'range': stmt = "%s AND id > %s" % (stmt, range[0]) if len(range) == 2: stmt = "%s AND id < %s" % (stmt, range[1]) else: stmt = "%s AND id = %s" % (stmt, range[0]) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_first_article(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT MIN(id) AS first_article FROM %s WHERE approved='Y'""" % (table_name) self.cursor.execute(stmt) minimum = self.cursor.fetchone()[0] if minimum is None: return 0 else: return minimum def get_group_stats(self, group_name): total, max, min = self.get_table_stats(self.get_table_name(group_name)) return (total, min, max, group_name) def get_table_stats(self, table_name): stmt = """ SELECT COUNT(id) AS total, MAX(id) AS maximum, MIN(id) AS minimum FROM %s WHERE approved='Y'""" % (table_name) self.cursor.execute(stmt) total, maximum, minimum = self.cursor.fetchone() if maximum is None: maximum = 0 if minimum is None: minimum = 0 return (total, maximum, minimum) def get_table_name(self, group_name): stmt = """ SELECT table_name FROM forums WHERE nntp_group_name LIKE '%s'""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) return self.cursor.fetchone()[0] def get_message_id(self, msg_num, group): return '<%s@%s>' % (msg_num, group) def get_notification_emails(self, forum_id): # open the configuration file fp = open("%s%s.php" % (settings.phorum_settings_path, forum_id), "r") content = fp.read() fp.close() # get the value of the configuration variable recipients = [] mod_code = moderator_regexp.search(content, 0).groups() if mod_code[2] == 'r' or mod_code[2] == 'a': # get the moderator emails from the forum_auth table stmt = """ SELECT email FROM forums_auth, forums_moderators WHERE user_id=id AND forum_id=%s""" % (forum_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) for row in result: recipients.append(row[0].strip()) return recipients def send_notifications(self, group_name, msg_id, thread_id, parent_id, msg_author, msg_email, msg_subject, msg_body): msg_tpl = """From: Phorum <%(recipient)s> To: %(recipient)s Subject: Moderate for %(forum_name)s at %(phorum_server_hostname)s Message: %(msg_id)s. Subject: %(msg_subject)s Author: %(msg_author)s Message: %(phorum_url)s/read.php?f=%(forum_id)s&i=%(msg_id)s&t=%(thread_id)s&admview=1 %(msg_body)s To delete this message use this URL: %(phorum_admin_url)s?page=easyadmin&action=del&type=quick&id=%(msg_id)s&num=1&thread=%(thread_id)s To edit this message use this URL: %(phorum_admin_url)s?page=edit&srcpage=easyadmin&id=%(msg_id)s&num=1&mythread=%(thread_id)s """ # get the forum_id for this group_name stmt = """ SELECT id, name FROM forums WHERE nntp_group_name='%s'""" % (group_name) self.cursor.execute(stmt) forum_id, forum_name = self.cursor.fetchone() forum_name.strip() # open the main configuration file fp = open("%sforums.php" % (settings.phorum_settings_path), "r") content = fp.read() fp.close() # regexps to get the content from the phorum configuration files phorum_url = url_regexp.search(content, 0).groups()[2] phorum_admin_url = admin_regexp.search(content, 0).groups()[2] phorum_server_hostname = server_regexp.search(content, 0).groups()[3] # connect to the SMTP server smtp = smtplib.SMTP('localhost') emails = self.get_notification_emails(forum_id) for recipient in emails: current_msg = msg_tpl % vars() smtp.sendmail("Phorum <%s>" % (recipient), recipient, current_msg) # XXX: Coding blind here. I really don't know much about how Phorum works with # XXX: sending forum postings as emails, but it's here. Let's call this a # XXX: temporary implementation. Should work fine, I guess. phorum_mail_code = mail_code_regexp.search(content, 0).groups()[3] notification_mail_tpl = """Message-ID: <%(random_msgid)s@%(phorum_server_hostname)s> From: %(msg_author)s %(msg_email)s Subject: %(msg_subject)s To: %(forum_name)s <%(email_list)s> Return-Path: <%(email_return)s> Reply-To: %(email_return)s X-Phorum-%(phorum_mail_code)s-Version: Phorum %(phorum_version)s X-Phorum-%(phorum_mail_code)s-Forum: %(forum_name)s X-Phorum-%(phorum_mail_code)s-Thread: %(thread_id)s X-Phorum-%(phorum_mail_code)s-Parent: %(parent_id)s This message was sent from: %(forum_name)s. <%(phorum_url)s/read.php?f=%(forum_id)s&i=%(msg_id)s&t=%(thread_id)s> ---------------------------------------------------------------- %(msg_body)s ---------------------------------------------------------------- Sent using Papercut version %(__VERSION__)s """ stmt = """ SELECT email_list, email_return FROM forums WHERE LENGTH(email_list) > 0 AND id=%s""" % (forum_id) num_rows = self.cursor.execute(stmt) if num_rows == 1: email_list, email_return = self.cursor.fetchone() msg_body = strutil.wrap(msg_body) if len(msg_email) > 0: msg_email = '<%s>' % msg_email else: msg_email = '' random_msgid = md5.new(str(time.clock())).hexdigest() # this is pretty ugly, right ? from papercut import __VERSION__ phorum_version = settings.phorum_version current_msg = notification_mail_tpl % vars() smtp.sendmail('%s %s' % (msg_author, msg_email), email_list, current_msg) smtp.quit() def get_NEWGROUPS(self, ts, group='%'): # since phorum doesn't record when each forum was created, we have no way of knowing this... return None def get_NEWNEWS(self, ts, group='*'): stmt = """ SELECT nntp_group_name, table_name FROM forums WHERE nntp_group_name='%s' ORDER BY nntp_group_name ASC""" % (group_name.replace('*', '%')) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) articles = [] for group, table in result: stmt = """ SELECT id FROM %s WHERE approved='Y' AND DATE_PART('epoch', datestamp) >= %s""" % (table, ts) num_rows = self.cursor.execute(stmt) if num_rows == 0: continue ids = list(self.cursor.fetchall()) for id in ids: articles.append("<%s@%s>" % (id, group)) if len(articles) == 0: return '' else: return "\r\n".join(articles) def get_GROUP(self, group_name): table_name = self.get_table_name(group_name) result = self.get_table_stats(table_name) return (result[0], result[2], result[1]) def get_LIST(self, username=""): stmt = """ SELECT nntp_group_name, table_name FROM forums WHERE LENGTH(nntp_group_name) > 0 ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) if len(result) == 0: return "" else: lists = [] for group_name, table in result: total, maximum, minimum = self.get_table_stats(table) if settings.server_type == 'read-only': lists.append("%s %s %s n" % (group_name, maximum, minimum)) else: lists.append("%s %s %s y" % (group_name, maximum, minimum)) return "\r\n".join(lists) def get_STAT(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id=%s""" % (table_name, id) return self.cursor.execute(stmt) def get_ARTICLE(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, author, email, subject, DATE_PART('epoch', datestamp) AS datestamp, body, parent FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id=B.id AND A.id=%s""" % (table_name, table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) if len(result[2]) == 0: author = result[1].strip() else: author = "%s <%s>" % (result[1].strip(), result[2].strip()) formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3].strip())) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[6] != 0: headers.append("References: <%s@%s>" % (result[6], group_name)) return ("\r\n".join(headers), strutil.format_body(result[5])) def get_LAST(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id < %s ORDER BY id DESC LIMIT 1, 0""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_NEXT(self, group_name, current_id): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' AND id > %s ORDER BY id ASC LIMIT 1, 0""" % (table_name, current_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None return self.cursor.fetchone()[0] def get_HEAD(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT id, author, email, subject, DATE_PART('epoch', datestamp) AS datestamp, parent FROM %s WHERE approved='Y' AND id=%s""" % (table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchone()) if len(result[2]) == 0: author = result[1].strip() else: author = "%s <%s>" % (result[1].strip(), result[2].strip()) formatted_time = strutil.get_formatted_time(time.localtime(result[4])) headers = [] headers.append("Path: %s" % (settings.nntp_hostname)) headers.append("From: %s" % (author)) headers.append("Newsgroups: %s" % (group_name)) headers.append("Date: %s" % (formatted_time)) headers.append("Subject: %s" % (result[3].strip())) headers.append("Message-ID: <%s@%s>" % (result[0], group_name)) headers.append("Xref: %s %s:%s" % (settings.nntp_hostname, group_name, result[0])) if result[5] != 0: headers.append("References: <%s@%s>" % (result[5], group_name)) return "\r\n".join(headers) def get_BODY(self, group_name, id): table_name = self.get_table_name(group_name) stmt = """ SELECT B.body FROM %s A, %s_bodies B WHERE A.id=B.id AND A.approved='Y' AND B.id=%s""" % (table_name, table_name, id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None else: return strutil.format_body(self.cursor.fetchone()[0]) def get_XOVER(self, group_name, start_id, end_id='ggg'): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, DATE_PART('epoch', datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id=B.id AND A.id >= %s""" % (table_name, table_name, start_id) if end_id != 'ggg': stmt = "%s AND A.id <= %s" % (stmt, end_id) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) overviews = [] for row in result: if row[3] == '': author = row[2].strip() else: author = "%s <%s>" % (row[2].strip(), row[3].strip()) formatted_time = strutil.get_formatted_time(time.localtime(row[5])) message_id = "<%s@%s>" % (row[0], group_name) line_count = len(row[6].split('\n')) xref = 'Xref: %s %s:%s' % (settings.nntp_hostname, group_name, row[0]) if row[1] != 0: reference = "<%s@%s>" % (row[1], group_name) else: reference = "" # message_number subject author date message_id reference bytes lines xref overviews.append("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (row[0], row[4].strip(), author, formatted_time, message_id, reference, len(strutil.format_body(row[6])), line_count, xref)) return "\r\n".join(overviews) def get_XPAT(self, group_name, header, pattern, start_id, end_id='ggg'): # XXX: need to actually check for the header values being passed as # XXX: not all header names map to column names on the tables table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, DATE_PART('epoch', datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND %s LIKE '%s' AND A.id = B.id AND A.id >= %s""" % (table_name, table_name, header, strutil.format_wildcards_sql(pattern), start_id) if end_id != 'ggg': stmt = "%s AND A.id <= %s" % (stmt, end_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None result = list(self.cursor.fetchall()) hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4].strip())) elif header.upper() == 'FROM': # XXX: totally broken with empty values for the email address hdrs.append('%s %s <%s>' % (row[0], row[2].strip(), row[3].strip())) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def get_LISTGROUP(self, group_name): table_name = self.get_table_name(group_name) stmt = """ SELECT id FROM %s WHERE approved='Y' ORDER BY id ASC""" % (table_name) self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s" % k for k in result]) def get_XGTITLE(self, pattern=None): stmt = """ SELECT nntp_group_name, description FROM forums WHERE LENGTH(nntp_group_name) > 0""" if pattern != None: stmt = stmt + """ AND nntp_group_name LIKE '%s'""" % (strutil.format_wildcards_sql(pattern)) stmt = stmt + """ ORDER BY nntp_group_name ASC""" self.cursor.execute(stmt) result = list(self.cursor.fetchall()) return "\r\n".join(["%s %s" % (k, v) for k, v in result]) def get_XHDR(self, group_name, header, style, range): table_name = self.get_table_name(group_name) stmt = """ SELECT A.id, parent, author, email, subject, DATE_PART('epoch', datestamp) AS datestamp, B.body FROM %s A, %s_bodies B WHERE A.approved='Y' AND A.id = B.id AND """ % (table_name, table_name) if style == 'range': stmt = '%s A.id >= %s' % (stmt, range[0]) if len(range) == 2: stmt = '%s AND A.id <= %s' % (stmt, range[1]) else: stmt = '%s A.id = %s' % (stmt, range[0]) if self.cursor.execute(stmt) == 0: return None result = self.cursor.fetchall() hdrs = [] for row in result: if header.upper() == 'SUBJECT': hdrs.append('%s %s' % (row[0], row[4].strip())) elif header.upper() == 'FROM': hdrs.append('%s %s <%s>' % (row[0], row[2].strip(), row[3].strip())) elif header.upper() == 'DATE': hdrs.append('%s %s' % (row[0], strutil.get_formatted_time(time.localtime(result[5])))) elif header.upper() == 'MESSAGE-ID': hdrs.append('%s <%s@%s>' % (row[0], row[0], group_name)) elif (header.upper() == 'REFERENCES') and (row[1] != 0): hdrs.append('%s <%s@%s>' % (row[0], row[1], group_name)) elif header.upper() == 'BYTES': hdrs.append('%s %s' % (row[0], len(row[6]))) elif header.upper() == 'LINES': hdrs.append('%s %s' % (row[0], len(row[6].split('\n')))) elif header.upper() == 'XREF': hdrs.append('%s %s %s:%s' % (row[0], settings.nntp_hostname, group_name, row[0])) if len(hdrs) == 0: return "" else: return "\r\n".join(hdrs) def do_POST(self, group_name, lines, ip_address, username=''): table_name = self.get_table_name(group_name) body = self.get_message_body(lines) author, email = from_regexp.search(lines, 0).groups() subject = subject_regexp.search(lines, 0).groups()[0].strip() # patch by Andreas Wegmann to fix the handling of unusual encodings of messages lines = mime_decode_header(re.sub(q_quote_multiline, "=?\\1?Q?\\2\\3?=", lines)) if lines.find('References') != -1: # get the 'modifystamp' value from the parent (if any) references = references_regexp.search(lines, 0).groups() parent_id, void = references[-1].strip().split('@') stmt = """ SELECT MAX(id) AS next_id FROM %s""" % (table_name) num_rows = self.cursor.execute(stmt) if num_rows == 0: new_id = 1 else: new_id = self.cursor.fetchone()[0] if new_id is None: new_id = 1 else: new_id = new_id + 1 stmt = """ SELECT id, thread, modifystamp FROM %s WHERE approved='Y' AND id=%s GROUP BY id""" % (table_name, parent_id) num_rows = self.cursor.execute(stmt) if num_rows == 0: return None parent_id, thread_id, modifystamp = self.cursor.fetchone() else: stmt = """ SELECT MAX(id) AS next_id, DATE_PART('epoch', CURRENT_TIMESTAMP()) FROM %s""" % (table_name) self.cursor.execute(stmt) new_id, modifystamp = self.cursor.fetchone() if new_id is None: new_id = 1 else: new_id = new_id + 1 modifystamp = int(modifystamp) parent_id = 0 thread_id = new_id stmt = """ INSERT INTO """ + table_name + """ ( id, datestamp, thread, parent, author, subject, email, host, email_reply, approved, msgid, modifystamp, userid ) VALUES ( %s, NOW(), %s, %s, '%s', '%s', '%s', '%s', 'N', 'Y', '', %s, 0 ) """ if not self.cursor.execute(stmt, (new_id, thread_id, parent_id, author.strip(), subject.strip(), email.strip(), ip_address, modifystamp,)): return None else: # insert into the '*_bodies' table stmt = """ INSERT INTO """ + table_name + """_bodies ( id, body, thread ) VALUES ( %s, '%s', %s )""" if not self.cursor.execute(stmt, (new_id, body, thread_id,)): # delete from 'table_name' before returning.. stmt = """ DELETE FROM %s WHERE id=%s""" % (table_name, new_id) self.cursor.execute(stmt) return None else: # alert forum moderators self.send_notifications(group_name, new_id, thread_id, parent_id, author.strip(), email.strip(), subject.strip(), body) return 1 papercut-0.9.13.orig/logs/0000755000175000017500000000000010327536617014370 5ustar lunarlunarpapercut-0.9.13.orig/logs/papercut.log0000644000175000017500000000000010327536616016703 0ustar lunarlunarpapercut-0.9.13.orig/cache/0000755000175000017500000000000010327536617014467 5ustar lunarlunarpapercut-0.9.13.orig/auth/0000755000175000017500000000000010327536617014365 5ustar lunarlunarpapercut-0.9.13.orig/auth/phorum_mysql_users.py0000644000175000017500000000337110327536616020722 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: phorum_mysql_users.py,v 1.5 2004/01/14 22:26:40 jpm Exp $ import MySQLdb import settings import crypt import md5 class Papercut_Auth: """ Authentication backend interface for the Phorum web message board software (http://phorum.org) This backend module tries to authenticate the users against the forums_auth table, which is used by Phorum to save its user based information, be it with a moderator level or not. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT password FROM forums_auth WHERE username='%s' """ % (username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] # somehow detect the version of phorum being used and guess the encryption type if len(db_password) == 32: result = (db_password != md5.new(password).hexdigest()) else: result = (db_password != crypt.crypt(password, password[:settings.PHP_CRYPT_SALT_LENGTH])) if result: settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/phorum_pgsql_users.py0000644000175000017500000000333110327536616020677 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: phorum_pgsql_users.py,v 1.3 2004/01/14 22:26:40 jpm Exp $ from pyPgSQL import PgSQL import settings import crypt import md5 class Papercut_Auth: """ Authentication backend interface for the Phorum web message board software (http://phorum.org) This backend module tries to authenticate the users against the forums_auth table, which is used by Phorum to save its user based information, be it with a moderator level or not. """ def __init__(self): self.conn = PgSQL.connect(database=settings.dbname, user=settings.dbuser) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT password FROM forums_auth WHERE username='%s' """ % (username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] # somehow detect the version of phorum being used and guess the encryption type if len(db_password) == 32: result = (db_password != md5.new(password).hexdigest()) else: result = (db_password != crypt.crypt(password, password[:settings.PHP_CRYPT_SALT_LENGTH])) if result: settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/phpbb_mysql_users.py0000644000175000017500000000275510327536616020510 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: phpbb_mysql_users.py,v 1.4 2003/09/19 03:11:51 jpm Exp $ import MySQLdb import settings import md5 class Papercut_Auth: """ Authentication backend interface for the phpBB web message board software (http://www.phpbb.com) This backend module tries to authenticate the users against the phpbb_users table. Many thanks to Chip McClure for the work on this file. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT user_password FROM %susers WHERE username='%s' """ % (settings.phpbb_table_prefix, username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] if db_password != md5.new(password).hexdigest(): settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/phpnuke_phpbb_mysql_users.py0000644000175000017500000000250510327536616022233 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. import MySQLdb import settings import md5 class Papercut_Auth: """ Authentication backend interface for the nuke port of phpBB (http://www.phpnuke.org) This backend module tries to authenticate the users against the nuke_users table. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT user_password FROM %susers WHERE username='%s' """ % (settings.nuke_table_prefix, username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] if db_password != md5.new(password).hexdigest(): settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/postnuke_phpbb_mysql_users.py0000644000175000017500000000262710327536616022436 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: postnuke_phpbb_mysql_users.py,v 1.1 2004/08/01 01:51:48 jpm Exp $ import MySQLdb import settings import md5 class Papercut_Auth: """ Authentication backend interface for the phpBB web message board software (http://www.phpbb.com) when used inside PostNuke. This backend module tries to authenticate the users against the phpbb_users table. """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT pn_pass FROM nuke_users WHERE pn_uname='%s' """ % (username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] if db_password != md5.new(password).hexdigest(): settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/mysql.py0000644000175000017500000000226610327536616016111 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: mysql.py,v 1.3 2003/04/26 00:24:55 jpm Exp $ import MySQLdb import settings class Papercut_Auth: """ Authentication backend interface """ def __init__(self): self.conn = MySQLdb.connect(host=settings.dbhost, db=settings.dbname, user=settings.dbuser, passwd=settings.dbpass) self.cursor = self.conn.cursor() def is_valid_user(self, username, password): stmt = """ SELECT password FROM papercut_groups_auth WHERE username='%s' """ % (username) num_rows = self.cursor.execute(stmt) if num_rows == 0 or num_rows is None: settings.logEvent('Error - Authentication failed for username \'%s\' (user not found)' % (username)) return 0 db_password = self.cursor.fetchone()[0] if db_password != password: settings.logEvent('Error - Authentication failed for username \'%s\' (incorrect password)' % (username)) return 0 else: return 1 papercut-0.9.13.orig/auth/__init__.py0000644000175000017500000000023610327536616016476 0ustar lunarlunar#!/usr/bin/env python # Copyright (c) 2002 Joao Prado Maia. See the LICENSE file for more information. # $Id: __init__.py,v 1.1 2002/04/04 23:10:20 jpm Exp $