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