Roundup Tracker

The following will download the messages spool for an issue as an mbox file. It's intended to be dropped into the extensions directory of a 0.8 or later tracker. It'll work with 0.7, but the registration interface has changed (the init bit at the end is 0.8-or-later).

The code:

from roundup.cgi.actions import Action

class MboxAction(Action):
    def handle(self):
        if self.classname != 'issue':
            raise ValueError, 'Can only download messages mbox for issues'
        if self.nodeid is None:
            raise ValueError, 'Can only download messages mbox for single issues'

        r = []
        a = r.append
        msg = self.db.msg
        user = self.db.user
        for msgid in self.db.issue.get(self.nodeid, 'messages'):
            author = msg.get(msgid, 'author')
            date = msg.get(msgid, 'date')
            sdate = date.pretty('%a, %d %b %Y %H:%M:%S +0000')
            a('From %s %s'%(user.get(author, 'address'), sdate))
            a('From: %s'%user.get(author, 'address'))
            a('Message-Id: %s'%msg.get(msgid, 'messageid'))
            inreplyto = msg.get(msgid, 'inreplyto')
            if inreplyto:
                a('In-Reply-To: %s'%inreplyto)
            body = msg.get(msgid, 'content').splitlines()
            for line in range(len(body)):
                if body[line].startswith('From '):
                    body[line] = '>'+body['line']
                a(body[line])
            a('\n')

        h = self.client.additional_headers
        h['Content-Type'] = 'application/mbox'

        self.client.header()
        if self.client.env['REQUEST_METHOD'] == 'HEAD':
            # all done, return a dummy string
            return 'dummy'

        return '\n'.join(r)


def init(tracker):
    tracker.registerAction('mbox', MboxAction)

-- RichardJones

From wiki Sat Jul 29 11:12:31 +1000 2006
From: wiki
Date: Sat, 29 Jul 2006 11:12:31 +1000
Subject: The usage bit
Message-ID: <20060729111231+1000@www.mechanicalcat.net>

To use it, I created a link:

<a href="#" tal:attributes="href string:issue${context/id}?@action=mbox" i18n:translate="">Download</a>

(should work fine in the issue.item.html template)

From twb Fri Jan 23 14:37:20 +1100 2009
From: Trent W. Buck <trentbuck@gmail.com>
Date: Tue, 20 Jan 2009 12:04:44 +1100
Subject: New version
Message-ID: <301vuyzt1v.fsf@Clio.twb.ath.cx>

I have worked on improving this extension, the code of which can be seen at

and the results of which can be seen at e.g.

I hope to improve mbox support further in future, but I don't intend to work on it in the near future. In particular:

I think the latter two are hard because AFAICT roundup "forgets" which messages are associated with which property changes and attachments.

Rouilj found the code referenced by Trent at: http://darcs.net/darcs-bugtracker in the extensions subdirectory. It is deployed to a 1.x roundup instance, but I am not sure which version. I am including the code below and have added an attachment at (aka [[attachment:darcs-messages_as_mbox.py]]).

The code is:

from email.message import Message
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.encoders import encode_base64

class MboxAction(Action):
    ## These get set in handle() by a dirty hack (see "setattr").
    activity = None             # issue / patch
    actor = None                # issue / patch
    assignedto = None           # issue / patch (used)
    creation = None             # issue / patch
    creator = None              # issue / patch
    darcswatchurl = None        # patch
    messages = None             # issue / patch (used)
    nosy = None                 # issue / patch (used)
    priority = None             # issue (used)
    status = None               # issue / patch (used)
    title = None                # issue / patch (used)
    topic = None                # issue (used)

    def handle(self):
        if self.classname != "issue" and self.classname != "patch":
            raise ValueError, "Only an issue or patch can become an mbox."
        if self.nodeid is None:
            raise ValueError, "Cannot iterate over multiple issues at once."

        ## This saves typing later.  It's naughty, because AFAICT
        ## self.db.issue.get() does some extra magic that isn't
        ## reproduced here.
        for (key, val) in self.db.getnode (self.classname, self.nodeid).items ():
            setattr (self, key, val)

        if not self.messages:
            raise ValueError, "Can't make an mbox without messages."
        self.client.additional_headers ["Content-Type"] = "application/mbox"
        self.client.header ()
        if self.client.env["REQUEST_METHOD"] == "HEAD":
            return None      # Stop now, as the mbox wasn't requested.

        mbox = []               # The accumulator.
        for message_id in self.messages:
            message = MIMEText (self.db.msg.get (message_id, "content"),
                                "plain", "utf-8") # hard-code as UTF-8 for now.
            files = self.db.msg.get (message_id, "files")
            if files:
                message_body = message
                message_body ["Content-Disposition"] = "inline"
                message = MIMEMultipart ()
                message.attach (message_body)
                for it in files:
                    attachment = Message ()
                    attachment.add_header ("Content-Disposition", "attachment",
                                           filename=self.db.file.get (it, "name"))
                    attachment ["Content-Type"] = self.db.file.get (it, "type")
                    attachment.set_payload (self.db.file.get (it, "content"))
                    encode_base64 (attachment) # bloated, but reliable.
                    message.attach (attachment)

            message ["Date"] = self.db.msg.get (message_id, "date").pretty ("%a, %d %b %Y %H:%M:%S +0000")
            message ["From"] = self.address (self.db.msg.get (message_id, "author"))
            it = self.db.msg.get (message_id, "recipients")
            if it: message ["CC"] = ", ".join ([self.address (recipient) for recipient in it])
            message ["Message-ID"] = self.db.msg.get(message_id, "messageid") or None
            message ["In-Reply-To"] = self.db.msg.get(message_id, "inreplyto") or None

            ## The remaining header fields are identical for all
            ## messages in the mbox, at least until such time as I
            ## work out how to reconcile the "journal" with the list
            ## of messages.
            message ["Reply-To"] = self.db.config.TRACKER_EMAIL
            message ["Subject"] = "[" + self.classname + self.nodeid + "] " + self.title
            if self.priority:
                message ["X-Roundup-Priority"] = self.db.priority.get (self.priority, "name")
            if self.status:
                message ["X-Roundup-Status"] = self.db.status.get (self.status, "name")
            if self.assignedto:
                message ["X-Roundup-Assigned-To"] = self.db.user.get (self.assignedto, "username")
            if self.nosy:
                message ["X-Roundup-Nosy-List"] = ", ".join([self.db.user.get (nose, "username")
                                                             for nose in self.nosy])
            if self.topic:
                message ["X-Roundup-Topics"] = ", ".join([self.db.keyword.get (topic, "name")
                                                             for topic in self.topic])

            mbox.append (message)

        return "\n".join ([str (message) for message in mbox])

    def address(self, x):
        get = self.db.user.get
        return (get(x, "realname") or get(x, "username")) + ":;"

def init(tracker):
    tracker.registerAction("mbox", MboxAction)

Use the attachment at [[attachment:darcs-messages_as_mbox.py]] if you are going to deploy this. Cut/paste from here will screw up the whitespace so critical for proper syntax in python. ---- CategoryActions