From 665d41d17ce640e5656b414b49d061f93fa8447d Mon Sep 17 00:00:00 2001 From: Joao Prado Maia Date: Sun, 1 Aug 2004 00:50:30 +0000 Subject: [PATCH] New 'maildir' storage backend (contributed by Scott Parish ) --- storage/maildir.py | 393 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100755 storage/maildir.py diff --git a/storage/maildir.py b/storage/maildir.py new file mode 100755 index 0000000..a760615 --- /dev/null +++ b/storage/maildir.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python +# Copyright (c) 2004 Scott Parish, Joao Prado Maia +# See the LICENSE file for more information. +# $Id: maildir.py,v 1.1 2004-08-01 00:50:30 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): + 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 +