2 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 Convert xml output of `be list --xml` into mbox format for browsing
19 with a mail reader. For example
20 $ be list --xml --status=all | be-xml-to-mbox | catmutt
22 mbox is a flat-file format, consisting of a series of messages.
23 Messages begin with a a From_ line, followed by RFC 822 email,
24 followed by a blank line.
27 #from mailbox import mbox, Message # the mailbox people really want an on-disk copy
29 from libbe.util.encoding import get_output_encoding
30 from libbe.util.utility import str_to_time as rfc2822_to_gmtime_integer
31 from time import asctime, gmtime
33 try: # import core module, Python >= 2.5
34 from xml.etree import ElementTree
35 except ImportError: # look for non-core module
36 from elementtree import ElementTree
37 from xml.sax.saxutils import unescape
40 DEFAULT_DOMAIN = "invalid.com"
41 DEFAULT_EMAIL = "dummy@" + DEFAULT_DOMAIN
42 DEFAULT_ENCODING = get_output_encoding()
44 def rfc2822_to_asctime(rfc2822_string):
45 """Convert an RFC 2822-fomatted string into a asctime string.
46 >>> rfc2822_to_asctime("Thu, 01 Jan 1970 00:00:00 +0000")
47 "Thu Jan 01 00:00:00 1970"
49 if rfc2822_string == "":
50 return asctime(gmtime(0))
51 return asctime(gmtime(rfc2822_to_gmtime_integer(rfc2822_string)))
53 class LimitedAttrDict (dict):
55 Dict with error checking, to avoid invalid bug/comment fields.
57 _attrs = [] # override with list of valid attribute names
58 def __init__(self, **kwargs):
60 for key,value in kwargs.items():
62 def __setitem__(self, key, item):
63 self._validate_key(key)
64 dict.__setitem__(self, key, item)
65 def _validate_key(self, key):
66 if key in self._attrs:
68 elif type(key) not in types.StringTypes:
69 raise TypeError, "Invalid attribute type %s for '%s'" % (type(key), key)
71 raise ValueError, "Invalid attribute name '%s'" % key
73 class Bug (LimitedAttrDict):
85 def print_to_mbox(self):
87 # otherwise, probably a `be show` uuid-only bug to avoid
89 name,addr = email.utils.parseaddr(self["creator"])
90 print "From %s %s" % (addr, rfc2822_to_asctime(self["created"]))
91 print "Message-id: <%s@%s>" % (self["uuid"], DEFAULT_DOMAIN)
92 print "Date: %s" % self["created"]
93 print "From: %s" % self["creator"]
94 print "Content-Type: %s; charset=%s" \
95 % ("text/plain", DEFAULT_ENCODING)
96 print "Content-Transfer-Encoding: 8bit"
97 print "Subject: %s: %s" % (self["short-name"], self["summary"])
98 if "extra-strings" in self:
99 for estr in self["extra-strings"]:
100 print "X-Extra-String: %s" % estr
102 print self["summary"]
104 if "comments" in self:
105 for comment in self["comments"]:
106 comment.print_to_mbox(self)
107 def init_from_etree(self, element):
108 assert element.tag == "bug", element.tag
109 for field in element.getchildren():
110 text = unescape(unicode(field.text).decode("unicode_escape").strip())
111 if field.tag == "comment":
113 comm.init_from_etree(field)
114 if "comments" in self:
115 self["comments"].append(comm)
117 self["comments"] = [comm]
118 elif field.tag == "extra-string":
119 if "extra-strings" in self:
120 self["extra-strings"].append(text)
122 self["extra-strings"] = [text]
124 self[field.tag] = text
128 return "<%s@%s>" % (id, DEFAULT_DOMAIN)
131 class Comment (LimitedAttrDict):
141 def print_to_mbox(self, bug=None):
144 bug[u"uuid"] = u"no-uuid"
145 name,addr = email.utils.parseaddr(self["author"])
146 print "From %s %s" % (addr, rfc2822_to_asctime(self["date"]))
147 if "uuid" in self: id = self["uuid"]
148 elif "alt-id" in self: id = self["alt-id"]
151 print "Message-id: %s" % wrap_id(id)
153 print "Alt-id: %s" % wrap_id(self["alt-id"])
154 print "Date: %s" % self["date"]
155 print "From: %s" % self["author"]
157 if "short-name" in self:
158 subject += self["short-name"]+u": "
160 subject += bug["summary"]
162 subject += u"no-subject"
163 print "Subject: %s" % subject
164 if "in-reply-to" not in self.keys():
165 self["in-reply-to"] = bug["uuid"]
166 print "In-Reply-To: %s" % wrap_id(self["in-reply-to"])
167 if "extra-strings" in self:
168 for estr in self["extra-strings"]:
169 print "X-Extra-String: %s" % estr
170 if self["content-type"].startswith("text/"):
171 print "Content-Transfer-Encoding: 8bit"
172 print "Content-Type: %s; charset=%s" \
173 % (self["content-type"], DEFAULT_ENCODING)
175 print "Content-Transfer-Encoding: base64"
176 print "Content-Type: %s;" % (self["content-type"])
180 def init_from_etree(self, element):
181 assert element.tag == "comment", element.tag
182 for field in element.getchildren():
183 text = unescape(unicode(field.text).decode("unicode_escape").strip())
184 if field.tag == "extra-string":
185 if "extra-strings" in self:
186 self["extra-strings"].append(text)
188 self["extra-strings"] = [text]
190 if field.tag == "body":
192 self[field.tag] = text
194 def print_to_mbox(element):
195 if element.tag == "bug":
197 b.init_from_etree(element)
199 elif element.tag == "comment":
201 c.init_from_etree(element)
203 elif element.tag in ["be-xml"]:
204 for elt in element.getchildren():
207 if __name__ == "__main__":
211 sys.stdin = codecs.getreader(DEFAULT_ENCODING)(sys.stdin)
212 sys.stdout = codecs.getwriter(DEFAULT_ENCODING)(sys.stdout)
214 if len(sys.argv) == 1: # no filename given, use stdin
215 xml_unicode = sys.stdin.read()
217 xml_unicode = codecs.open(sys.argv[1], "r", DEFAULT_ENCODING).read()
218 xml_str = xml_unicode.encode("unicode_escape").replace(r"\n", "\n")
219 elist = ElementTree.XML(xml_str)