* add support for  
[rss2maildir.git] / rss2maildir.py
1 #!/usr/bin/python
2 # coding=utf8
3
4 # rss2maildir.py - RSS feeds to Maildir 1 email per item
5 # Copyright (C) 2007  Brett Parker <iDunno@sommitrealweird.co.uk>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import mailbox
21 import sys
22 import os
23 import stat
24 import urllib
25
26 import feedparser
27
28 from email.MIMEMultipart import MIMEMultipart
29 from email.MIMEText import MIMEText
30
31 import datetime
32 import random
33 import string
34 import textwrap
35
36 import socket
37
38 from optparse import OptionParser
39 from ConfigParser import SafeConfigParser
40
41 from base64 import b64encode
42 import md5
43
44 import cgi
45 import dbm
46
47 from HTMLParser import HTMLParser
48
49 entities = {
50     "amp": "&",
51     "lt": "<",
52     "gt": ">",
53     "pound": "£",
54     "copy": "©",
55     "apos": "'",
56     "quote": "\"",
57     "nbsp": " ",
58     }
59
60 class HTML2Text(HTMLParser):
61     
62     def __init__(self):
63         self.inheadingone = False
64         self.inheadingtwo = False
65         self.inotherheading = False
66         self.inparagraph = True
67         self.inlink = False
68         self.text = ""
69         self.currentparagraph = ""
70         self.headingtext = ""
71         HTMLParser.__init__(self)
72
73     def handle_starttag(self, tag, attrs):
74         if tag.lower() == "h1":
75             self.inheadingone = True
76             self.inparagraph = False
77         elif tag.lower() == "h2":
78             self.inheadingtwo = True
79             self.inparagraph = False
80         elif tag.lower() in ["h3", "h4", "h5", "h6"]:
81             self.inotherheading = True
82             self.inparagraph = False
83         elif tag.lower() == "a":
84             self.inlink = True
85         elif tag.lower() == "br":
86             self.text = self.text + "\n"
87         elif tag.lower() == "p":
88             if self.text != "":
89                 self.text = self.text + "\n\n"
90                 self.currentparagraph = ""
91                 self.inparagraph = True
92
93     def handle_startendtag(self, tag, attrs):
94         if tag.lower() == "br":
95             self.text = self.text + "\n"
96
97     def handle_endtag(self, tag):
98         if tag.lower() == "h1":
99             self.inheadingone = False
100             self.text = self.text + self.headingtext + "\n" + "=" * len(self.headingtext)
101             self.headingtext = ""
102         elif tag.lower() == "h2":
103             self.inheadingtwo = False
104             self.text = self.text + self.headingtext + "\n" + "-" * len(self.headingtext)
105             self.headingtext = ""
106         elif tag.lower() in ["h3", "h4", "h5", "h6"]:
107             self.inotherheading = False
108             self.text = self.text + self.headingtext + "\n" + "~" * len(self.headingtext)
109             self.headingtext = ""
110         elif tag.lower() == "p":
111             self.text = self.text + "\n".join(textwrap.wrap(self.currentparagraph, 70))
112             self.inparagraph = False
113
114     def handle_data(self, data):
115         if not self.inheadingone and not self.inheadingtwo and not self.inotherheading and not self.inparagraph:
116             self.text = self.text + data.strip() + " "
117         elif self.inparagraph:
118             self.currentparagraph = self.currentparagraph + data.strip() + " "
119         else:
120             self.headingtext = self.headingtext + data.strip() + " "
121
122     def handle_entityref(self, name):
123         if entities.has_key(name.lower()):
124             self.text = self.text + entities[name.lower()]
125         else:
126             self.text = self.text + "&" + name + ";"
127
128     def gettext(self):
129         data = self.text
130         if self.inparagraph:
131             data = data + "\n".join(textwrap.wrap(self.currentparagraph, 70))
132         return data
133
134 def parse_and_deliver(maildir, url, statedir):
135     md = mailbox.Maildir(maildir)
136     fp = feedparser.parse(url)
137     db = dbm.open(os.path.join(statedir, "seen"), "c")
138     for item in fp["items"]:
139         # have we seen it before?
140         # need to work out what the content is first...
141
142         if item.has_key("content"):
143             content = item["content"][0]["value"]
144         else:
145             content = item["summary"]
146
147         md5sum = md5.md5(content.encode("utf8")).hexdigest()
148
149         if db.has_key(item["link"]):
150             data = db[item["link"]]
151             data = cgi.parse_qs(data)
152             if data["contentmd5"][0] == md5sum:
153                 continue
154
155         try:
156             author = item["author"]
157         except:
158             author = url
159
160         # create a basic email message
161         msg = MIMEMultipart("alternative")
162         messageid = "<" + datetime.datetime.now().strftime("%Y%m%d%H%M") + "." + "".join([random.choice(string.ascii_letters + string.digits) for a in range(0,6)]) + "@" + socket.gethostname() + ">"
163         msg.add_header("Message-ID", messageid)
164         msg.set_unixfrom("\"%s\" <rss2maildir@localhost>" %(url))
165         msg.add_header("From", "\"%s\" <rss2maildir@localhost>" %(author))
166         msg.add_header("To", "\"%s\" <rss2maildir@localhost>" %(url))
167         createddate = datetime.datetime(*item["updated_parsed"][0:6]).strftime("%a, %e %b %Y %T -0000")
168         msg.add_header("Date", createddate)
169         msg.add_header("Subject", item["title"])
170         msg.set_default_type("text/plain")
171
172         htmlpart = MIMEText(content.encode("utf8"), "html", "utf8")
173         textparser = HTML2Text()
174         textparser.feed(content.encode("utf8"))
175         textcontent = textparser.gettext()
176         textpart = MIMEText(textcontent, "plain", "utf8")
177         msg.attach(textpart)
178         msg.attach(htmlpart)
179
180         # start by working out the filename we should be writting to, we do
181         # this following the normal maildir style rules
182         fname = str(os.getpid()) + "." + socket.gethostname() + "." + "".join([random.choice(string.ascii_letters + string.digits) for a in range(0,10)]) + "." + datetime.datetime.now().strftime('%s')
183         fn = os.path.join(maildir, "tmp", fname)
184         fh = open(fn, "w")
185         fh.write(msg.as_string())
186         fh.close()
187         # now move it in to the new directory
188         newfn = os.path.join(maildir, "new", fname)
189         os.link(fn, newfn)
190         os.unlink(fn)
191
192         # now add to the database about the item
193         data = urllib.urlencode((("message-id", messageid), ("created", createddate), ("contentmd5", md5sum)))
194         db[item["link"]] = data
195
196     db.close()
197
198 # first off, parse the command line arguments
199
200 oparser = OptionParser()
201 oparser.add_option(
202     "-c", "--conf", dest="conf",
203     help="location of config file"
204     )
205 oparser.add_option(
206     "-s", "--statedir", dest="statedir",
207     help="location of directory to store state in"
208     )
209
210 (options, args) = oparser.parse_args()
211
212 # check for the configfile
213
214 configfile = None
215
216 if options.conf != None:
217     # does the file exist?
218     try:
219         os.stat(options.conf)
220         configfile = options.conf
221     except:
222         # should exit here as the specified file doesn't exist
223         sys.stderr.write("Config file %s does not exist. Exiting.\n" %(options.conf,))
224         sys.exit(2)
225 else:
226     # check through the default locations
227     try:
228         os.stat("%s/.rss2maildir.conf" %(os.environ["HOME"],))
229         configfile = "%s/.rss2maildir.conf" %(os.environ["HOME"],)
230     except:
231         try:
232             os.stat("/etc/rss2maildir.conf")
233             configfile = "/etc/rss2maildir.conf"
234         except:
235             sys.stderr.write("No config file found. Exiting.\n")
236             sys.exit(2)
237
238 # Right - if we've got this far, we've got a config file, now for the hard
239 # bits...
240
241 scp = SafeConfigParser()
242 scp.read(configfile)
243
244 maildir_root = "RSSMaildir"
245 state_dir = "state"
246
247 if options.statedir != None:
248     state_dir = options.statedir
249     try:
250         mode = os.stat(state_dir)[stat.ST_MODE]
251         if not stat.S_ISDIR(mode):
252             sys.stderr.write("State directory (%s) is not a directory\n" %(state_dir))
253             sys.exit(1)
254     except:
255         # try to make the directory
256         try:
257             os.mkdir(state_dir)
258         except:
259             sys.stderr.write("Couldn't create statedir %s" %(state_dir))
260             sys.exit(1)
261 elif scp.has_option("general", "state_dir"):
262     new_state_dir = scp.get("general", "state_dir")
263     try:
264         mode = os.stat(state_dir)[stat.ST_MODE]
265         if not stat.S_ISDIR(mode):
266             sys.stderr.write("State directory (%s) is not a directory\n" %(state_dir))
267             sys.exit(1)
268     except:
269         # try to create it
270         try:
271             os.mkdir(new_state_dir)
272             state_dir = new_state_dir
273         except:
274             sys.stderr.write("Couldn't create state directory %s\n" %(new_state_dir))
275             sys.exit(1)
276 else:
277     try:
278         mode = os.stat(state_dir)[stat.ST_MODE]
279         if not stat.S_ISDIR(mode):
280             sys.stderr.write("State directory %s is not a directory\n" %(state_dir))
281             sys.exit(1)
282     except:
283         try:
284             os.mkdir(state_dir)
285         except:
286             sys.stderr.write("State directory %s could not be created\n" %(state_dir))
287             sys.exit(1)
288
289 if scp.has_option("general", "maildir_root"):
290     maildir_root = scp.get("general", "maildir_root")
291
292 try:
293     mode = os.stat(maildir_root)[stat.ST_MODE]
294     if not stat.S_ISDIR(mode):
295         sys.stderr.write("Maildir Root %s is not a directory\n" %(maildir_root))
296         sys.exit(1)
297 except:
298     try:
299         os.mkdir(maildir_root)
300     except:
301         sys.stderr.write("Couldn't create Maildir Root %s\n" %(maildir_root))
302         sys.exit(1)
303
304 feeds = scp.sections()
305 try:
306     feeds.remove("general")
307 except:
308     pass
309
310 for section in feeds:
311     # check if the directory exists
312     maildir = None
313     try:
314         maildir = scp.get(section, "maildir")
315     except:
316         maildir = section
317
318     maildir = urllib.urlencode(((section, maildir),)).split("=")[1]
319     maildir = os.path.join(maildir_root, maildir)
320
321     try:
322         exists = os.stat(maildir)
323         if stat.S_ISDIR(exists[stat.ST_MODE]):
324             # check if there's a new, cur and tmp directory
325             try:
326                 mode = os.stat(os.path.join(maildir, "cur"))[stat.ST_MODE]
327             except:
328                 os.mkdir(os.path.join(maildir, "cur"))
329                 if not stat.S_ISDIR(mode):
330                     sys.stderr.write("Broken maildir: %s\n" %(maildir))
331             try:
332                 mode = os.stat(os.path.join(maildir, "tmp"))[stat.ST_MODE]
333             except:
334                 os.mkdir(os.path.join(maildir, "tmp"))
335                 if not stat.S_ISDIR(mode):
336                     sys.stderr.write("Broken maildir: %s\n" %(maildir))
337             try:
338                 mode = os.stat(os.path.join(maildir, "new"))[stat.ST_MODE]
339                 if not stat.S_ISDIR(mode):
340                     sys.stderr.write("Broken maildir: %s\n" %(maildir))
341             except:
342                 os.mkdir(os.path.join(maildir, "new"))
343         else:
344             sys.stderr.write("Broken maildir: %s\n" %(maildir))
345     except:
346         try:
347             os.mkdir(maildir)
348         except:
349             sys.stderr.write("Couldn't create root maildir %s\n" %(maildir))
350             sys.exit(1)
351         try:
352             os.mkdir(os.path.join(maildir, "new"))
353             os.mkdir(os.path.join(maildir, "cur"))
354             os.mkdir(os.path.join(maildir, "tmp"))
355         except:
356             sys.stderr.write("Couldn't create required maildir directories for %s\n" %(section,))
357             sys.exit(1)
358
359     # right - we've got the directories, we've got the section, we know the
360     # url... lets play!
361
362     parse_and_deliver(maildir, section, state_dir)