Add rss2email v2.64
[rss2email.git] / rss2email.py
1 #!/usr/bin/python
2 """rss2email: get RSS feeds emailed to you
3 http://rss2email.infogami.com
4
5 Usage:
6   new [emailaddress] (create new feedfile)
7   email newemailaddress (update default email)
8   run [--no-send] [num]
9   add feedurl [emailaddress]
10   list
11   delete n
12 """
13 __version__ = "2.64"
14 __author__ = "Aaron Swartz (me@aaronsw.com)"
15 __copyright__ = "(C) 2004 Aaron Swartz. GNU GPL 2 or 3."
16 ___contributors__ = ["Dean Jackson", "Brian Lalor", "Joey Hess", 
17                      "Matej Cepl", "Martin 'Joey' Schulze", 
18                      "Marcel Ackermann (http://www.DreamFlasher.de)", 
19                      "Lindsey Smith (lindsey.smith@gmail.com)" ]
20
21 ### Vaguely Customizable Options ###
22
23 # The email address messages are from by default:
24 DEFAULT_FROM = "bozo@dev.null.invalid"
25
26 # 1: Send text/html messages when possible.
27 # 0: Convert HTML to plain text.
28 HTML_MAIL = 0
29
30 # 1: Only use the DEFAULT_FROM address.
31 # 0: Use the email address specified by the feed, when possible.
32 FORCE_FROM = 0
33
34 # 1: Receive one email per post.
35 # 0: Receive an email every time a post changes.
36 TRUST_GUID = 1
37
38 # 1: Generate Date header based on item's date, when possible.
39 # 0: Generate Date header based on time sent.
40 DATE_HEADER = 0
41
42 # A tuple consisting of some combination of
43 # ('issued', 'created', 'modified', 'expired')
44 # expressing ordered list of preference in dates 
45 # to use for the Date header of the email.
46 DATE_HEADER_ORDER = ('modified', 'issued', 'created')
47
48 # 1: Apply Q-P conversion (required for some MUAs).
49 # 0: Send message in 8-bits.
50 # http://cr.yp.to/smtp/8bitmime.html
51 QP_REQUIRED = 0
52
53 # 1: Name feeds as they're being processed.
54 # 0: Keep quiet.
55 VERBOSE = 0
56
57 # 1: Use the publisher's email if you can't find the author's.
58 # 0: Just use the DEFAULT_FROM email instead.
59 USE_PUBLISHER_EMAIL = 0
60
61 # 1: Use SMTP_SERVER to send mail.
62 # 0: Call /usr/sbin/sendmail to send mail.
63 SMTP_SEND = 0
64
65 SMTP_SERVER = "smtp.yourisp.net:25"
66 AUTHREQUIRED = 0 # if you need to use SMTP AUTH set to 1
67 SMTP_USER = 'username'  # for SMTP AUTH, set SMTP username here
68 SMTP_PASS = 'password'  # for SMTP AUTH, set SMTP password here
69
70 # Set this to add a bonus header to all emails (start with '\n').
71 BONUS_HEADER = ''
72 # Example: BONUS_HEADER = '\nApproved: joe@bob.org'
73
74 # Set this to override From addresses. Keys are feed URLs, values are new titles.
75 OVERRIDE_FROM = {}
76
77 # Set this to override the timeout (in seconds) for feed server response
78 FEED_TIMEOUT = 60
79
80 # Optional CSS styling
81 USE_CSS_STYLING = 0
82 STYLE_SHEET='h1 {font: 18pt Georgia, "Times New Roman";} body {font: 12pt Arial;} a:link {font: 12pt Arial; font-weight: bold; color: #0000cc} blockquote {font-family: monospace; }  .header { background: #e0ecff; border-bottom: solid 4px #c3d9ff; padding: 5px; margin-top: 0px; color: red;} .header a { font-size: 20px; text-decoration: none; } .footer { background: #c3d9ff; border-top: solid 4px #c3d9ff; padding: 5px; margin-bottom: 0px; } #entry {border: solid 4px #c3d9ff; } #body { margin-left: 5px; margin-right: 5px; }'
83
84 # If you have an HTTP Proxy set this in the format 'http://your.proxy.here:8080/'
85 PROXY=""
86
87 # To most correctly encode emails with international characters, we iterate through the list below and use the first character set that works
88 # Eventually (and theoretically) ISO-8859-1 and UTF-8 are our catch-all failsafes
89 CHARSET_LIST='US-ASCII', 'BIG5', 'ISO-2022-JP', 'ISO-8859-1', 'UTF-8'
90
91 from email.MIMEText import MIMEText
92 from email.Header import Header
93 from email.Utils import parseaddr, formataddr
94                          
95 # Note: You can also override the send function.
96
97 def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
98         """Send an email.
99         
100         All arguments should be Unicode strings (plain ASCII works as well).
101         
102         Only the real name part of sender and recipient addresses may contain
103         non-ASCII characters.
104         
105         The email will be properly MIME encoded and delivered though SMTP to
106         localhost port 25.  This is easy to change if you want something different.
107         
108         The charset of the email will be the first one out of the list
109         that can represent all the characters occurring in the email.
110         """
111
112         # Header class is smart enough to try US-ASCII, then the charset we
113         # provide, then fall back to UTF-8.
114         header_charset = 'ISO-8859-1'
115         
116         # We must choose the body charset manually
117         for body_charset in CHARSET_LIST:
118             try:
119                 body.encode(body_charset)
120             except (UnicodeError, LookupError):
121                 pass
122             else:
123                 break
124
125         # Split real name (which is optional) and email address parts
126         sender_name, sender_addr = parseaddr(sender)
127         recipient_name, recipient_addr = parseaddr(recipient)
128         
129         # We must always pass Unicode strings to Header, otherwise it will
130         # use RFC 2047 encoding even on plain ASCII strings.
131         sender_name = str(Header(unicode(sender_name), header_charset))
132         recipient_name = str(Header(unicode(recipient_name), header_charset))
133         
134         # Make sure email addresses do not contain non-ASCII characters
135         sender_addr = sender_addr.encode('ascii')
136         recipient_addr = recipient_addr.encode('ascii')
137         
138         # Create the message ('plain' stands for Content-Type: text/plain)
139         msg = MIMEText(body.encode(body_charset), contenttype, body_charset)
140         msg['To'] = formataddr((recipient_name, recipient_addr))
141         msg['Subject'] = Header(unicode(subject), header_charset)
142         for hdr in extraheaders.keys():
143                 msg[hdr] = Header(unicode(extraheaders[hdr], header_charset))
144                 
145         fromhdr = formataddr((sender_name, sender_addr))
146         msg['From'] = fromhdr
147                 
148         msg_as_string = msg.as_string()
149         if QP_REQUIRED:
150                 ins, outs = SIO(msg_as_string), SIO()
151                 mimify.mimify(ins, outs)
152                 msg_as_string = outs.getvalue()
153                 
154         if SMTP_SEND:
155                 if not smtpserver: 
156                         import smtplib
157                         
158                         try:
159                                 smtpserver = smtplib.SMTP(SMTP_SERVER)
160                         except KeyboardInterrupt:
161                                 raise
162                         except Exception, e:
163                                 print >>warn, ""
164                                 print >>warn, ('Fatal error: could not connect to mail server "%s"' % SMTP_SERVER)
165                                 if hasattr(e, 'reason'):
166                                         print >>warn, "Reason:", e.reason
167                                 sys.exit(1)
168                                         
169                         if AUTHREQUIRED:
170                                 try:
171                                         smtpserver.ehlo()
172                                         smtpserver.starttls()
173                                         smtpserver.ehlo()
174                                         smtpserver.login(SMTP_USER, SMTP_PASS)
175                                 except KeyboardInterrupt:
176                                         raise
177                                 except Exception, e:
178                                         print >>warn, ""
179                                         print >>warn, ('Fatal error: could not authenticate with mail server "%s" as user "%s"' % (SMTP_SERVER, SMTP_USER))
180                                         if hasattr(e, 'reason'):
181                                                 print >>warn, "Reason:", e.reason
182                                         sys.exit(1)
183                                         
184                 smtpserver.sendmail(sender, recipient, msg_as_string)
185                 return smtpserver
186
187         else:
188                 try:
189                         i, o = os.popen2(["/usr/sbin/sendmail", recipient])
190                         i.write(msg_as_string)
191                         i.close(); o.close()
192                         del i, o
193                 except:
194                         print '''Error attempting to send email via sendmail. Possibly you need to configure your config.py to use a SMTP server? Please refer to the rss2email documentation or website (http://rss2email.infogami.com) for complete documentation of config.py. The options below may suffice for configuring email:
195 # 1: Use SMTP_SERVER to send mail.
196 # 0: Call /usr/sbin/sendmail to send mail.
197 SMTP_SEND = 0
198
199 SMTP_SERVER = "smtp.yourisp.net:25"
200 AUTHREQUIRED = 0 # if you need to use SMTP AUTH set to 1
201 SMTP_USER = 'username'  # for SMTP AUTH, set SMTP username here
202 SMTP_PASS = 'password'  # for SMTP AUTH, set SMTP password here
203 '''
204                         sys.exit(1)
205                 return None
206
207 ## html2text options ##
208
209 # Use Unicode characters instead of their ascii psuedo-replacements
210 UNICODE_SNOB = 0
211
212 # Put the links after each paragraph instead of at the end.
213 LINKS_EACH_PARAGRAPH = 0
214
215 # Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
216 BODY_WIDTH = 0
217
218 ### Load the Options ###
219
220 # Read options from config file if present.
221 import sys
222 sys.path.append(".")
223 try:
224         from config import *
225 except:
226         pass
227         
228 ### Import Modules ###
229
230 import cPickle as pickle, md5, time, os, traceback, urllib2, sys, types
231 unix = 0
232 try:
233         import fcntl
234         unix = 1
235 except:
236         pass
237                 
238 import socket; socket_errors = []
239 for e in ['error', 'gaierror']:
240         if hasattr(socket, e): socket_errors.append(getattr(socket, e))
241 import mimify; from StringIO import StringIO as SIO; mimify.CHARSET = 'utf-8'
242
243 import feedparser
244 feedparser.USER_AGENT = "rss2email/"+__version__+ " +http://www.aaronsw.com/2002/rss2email/"
245
246 import html2text as h2t
247
248 h2t.UNICODE_SNOB = UNICODE_SNOB
249 h2t.LINKS_EACH_PARAGRAPH = LINKS_EACH_PARAGRAPH
250 h2t.BODY_WIDTH = BODY_WIDTH
251 html2text = h2t.html2text
252
253 ### Utility Functions ###
254
255 import threading
256 class TimeoutError(Exception): pass
257
258 class InputError(Exception): pass
259
260 def timelimit(timeout, function):
261 #    def internal(function):
262         def internal2(*args, **kw):
263             """
264             from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878
265             """
266             class Calculator(threading.Thread):
267                 def __init__(self):
268                     threading.Thread.__init__(self)
269                     self.result = None
270                     self.error = None
271                 
272                 def run(self):
273                     try:
274                         self.result = function(*args, **kw)
275                     except:
276                         self.error = sys.exc_info()
277             
278             c = Calculator()
279             c.setDaemon(True) # don't hold up exiting
280             c.start()
281             c.join(timeout)
282             if c.isAlive():
283                 raise TimeoutError
284             if c.error:
285                 raise c.error[0], c.error[1]
286             return c.result
287         return internal2
288 #    return internal
289     
290
291 warn = sys.stderr
292
293 def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u''))
294 def ishtml(t): return type(t) is type(())
295 def contains(a,b): return a.find(b) != -1
296 def unu(s): # I / freakin' hate / that unicode
297         if type(s) is types.UnicodeType: return s.encode('utf-8')
298         else: return s
299
300 def quote822(s):
301         """Quote names in email according to RFC822."""
302         return '"' + unu(s).replace("\\", "\\\\").replace('"', '\\"') + '"'
303
304 def header7bit(s):
305         """QP_CORRUPT headers."""
306         #return mimify.mime_encode_header(s + ' ')[:-1]
307         # XXX due to mime_encode_header bug
308         import re
309         p = re.compile('=\n([^ \t])');
310         return p.sub(r'\1', mimify.mime_encode_header(s + ' ')[:-1])
311
312 ### Parsing Utilities ###
313
314 def getContent(entry, HTMLOK=0):
315         """Select the best content from an entry, deHTMLizing if necessary.
316         If raw HTML is best, an ('HTML', best) tuple is returned. """
317         
318         # How this works:
319         #  * We have a bunch of potential contents. 
320         #  * We go thru looking for our first choice. 
321         #    (HTML or text, depending on HTMLOK)
322         #  * If that doesn't work, we go thru looking for our second choice.
323         #  * If that still doesn't work, we just take the first one.
324         #
325         # Possible future improvement:
326         #  * Instead of just taking the first one
327         #    pick the one in the "best" language.
328         #  * HACK: hardcoded HTMLOK, should take a tuple of media types
329         
330         conts = entry.get('content', [])
331         
332         if entry.get('summary_detail', {}):
333                 conts += [entry.summary_detail]
334         
335         if conts:
336                 if HTMLOK:
337                         for c in conts:
338                                 if contains(c.type, 'html'): return ('HTML', c.value)
339         
340                 if not HTMLOK: # Only need to convert to text if HTML isn't OK
341                         for c in conts:
342                                 if contains(c.type, 'html'):
343                                         return html2text(c.value)
344                 
345                 for c in conts:
346                         if c.type == 'text/plain': return c.value
347         
348                 return conts[0].value   
349         
350         return ""
351
352 def getID(entry):
353         """Get best ID from an entry."""
354         if TRUST_GUID:
355                 if 'id' in entry and entry.id: return entry.id
356
357         content = getContent(entry)
358         if content and content != "\n": return md5.new(unu(content)).hexdigest()
359         if 'link' in entry: return entry.link
360         if 'title' in entry: return md5.new(unu(entry.title)).hexdigest()
361
362 def getName(r, entry):
363         """Get the best name."""
364
365         feed = r.feed
366         if hasattr(r, "url") and r.url in OVERRIDE_FROM.keys():
367                 return OVERRIDE_FROM[r.url]
368         
369         name = feed.get('title', '')
370
371         if 'name' in entry.get('author_detail', []): # normally {} but py2.1
372                 if entry.author_detail.name:
373                         if name: name += ": "
374                         det=entry.author_detail.name
375                         try:
376                             name +=  entry.author_detail.name
377                         except UnicodeDecodeError:
378                             name +=  unicode(entry.author_detail.name, 'utf-8')
379
380         elif 'name' in feed.get('author_detail', []):
381                 if feed.author_detail.name:
382                         if name: name += ", "
383                         name += feed.author_detail.name
384         
385         return name
386
387 def getEmail(feed, entry):
388         """Get the best email_address."""
389
390         if FORCE_FROM: return DEFAULT_FROM
391         
392         if 'email' in entry.get('author_detail', []):
393                 return entry.author_detail.email
394         
395         if 'email' in feed.get('author_detail', []):
396                 return feed.author_detail.email
397                 
398         #TODO: contributors
399         
400         if USE_PUBLISHER_EMAIL:
401                 if 'email' in feed.get('publisher_detail', []):
402                         return feed.publisher_detail.email
403                 
404                 if feed.get("errorreportsto", ''):
405                         return feed.errorreportsto
406                         
407         return DEFAULT_FROM
408
409 ### Simple Database of Feeds ###
410
411 class Feed:
412         def __init__(self, url, to):
413                 self.url, self.etag, self.modified, self.seen = url, None, None, {}
414                 self.to = to            
415
416 def load(lock=1):
417         if not os.path.exists(feedfile):
418                 print 'Feedfile "%s" does not exist.  If you\'re using r2e for the first time, you' % feedfile
419                 print "have to run 'r2e new' first."
420                 sys.exit(1)
421         try:
422                 feedfileObject = open(feedfile, 'r')
423         except IOError, e:
424                 print "Feedfile could not be opened: %s" % e
425                 sys.exit(1)
426         feeds = pickle.load(feedfileObject)
427         if lock:
428                 locktype = 0
429                 if unix:
430                         locktype = fcntl.LOCK_EX
431                         if (sys.platform.find('sunos')): locktype = fcntl.LOCK_SH
432                         fcntl.flock(feedfileObject.fileno(), locktype)
433                 #HACK: to deal with lock caching
434                 feedfileObject = open(feedfile, 'r')
435                 feeds = pickle.load(feedfileObject)
436                 if unix: fcntl.flock(feedfileObject.fileno(), locktype)
437
438         return feeds, feedfileObject
439
440 def unlock(feeds, feedfileObject):
441         if not unix: 
442                 pickle.dump(feeds, open(feedfile, 'w'))
443         else:   
444                 pickle.dump(feeds, open(feedfile+'.tmp', 'w'))
445                 os.rename(feedfile+'.tmp', feedfile)
446                 fcntl.flock(feedfileObject.fileno(), fcntl.LOCK_UN)
447
448 #@timelimit(FEED_TIMEOUT)               
449 def parse(url, etag, modified):
450         if PROXY == '':
451                 return feedparser.parse(url, etag, modified)
452         else:
453                 proxy = urllib2.ProxyHandler( {"http":PROXY} )
454                 return feedparser.parse(url, etag, modified, handlers = [proxy])        
455         
456                 
457 ### Program Functions ###
458
459 def add(*args):
460         if len(args) == 2 and contains(args[1], '@') and not contains(args[1], '://'):
461                 urls, to = [args[0]], args[1]
462         else:
463                 urls, to = args, None
464         
465         feeds, feedfileObject = load()
466         if (feeds and not isstr(feeds[0]) and to is None) or (not len(feeds) and to is None):
467                 print "No email address has been defined. Please run 'r2e email emailaddress' or"
468                 print "'r2e add url emailaddress'."
469                 sys.exit(1)
470         for url in urls: feeds.append(Feed(url, to))
471         unlock(feeds, feedfileObject)
472
473 def run(num=None):
474         feeds, feedfileObject = load()
475         try:
476                 # We store the default to address as the first item in the feeds list.
477                 # Here we take it out and save it for later.
478                 default_to = ""
479                 if feeds and isstr(feeds[0]): default_to = feeds[0]; ifeeds = feeds[1:] 
480                 else: ifeeds = feeds
481                 
482                 if num: ifeeds = [feeds[num]]
483                 feednum = 0
484                 
485                 smtpserver = None
486                 
487                 for f in ifeeds:
488                         try: 
489                                 feednum += 1
490                                 if VERBOSE: print >>warn, 'I: Processing [%d] "%s"' % (feednum, f.url)
491                                 r = {}
492                                 try:
493                                         r = timelimit(FEED_TIMEOUT, parse)(f.url, f.etag, f.modified)
494                                 except TimeoutError:
495                                         print >>warn, 'W: feed [%d] "%s" timed out' % (feednum, f.url)
496                                         continue
497                                 
498                                 # Handle various status conditions, as required
499                                 if 'status' in r:
500                                         if r.status == 301: f.url = r['url']
501                                         elif r.status == 410:
502                                                 print >>warn, "W: feed gone; deleting", f.url
503                                                 feeds.remove(f)
504                                                 continue
505                                 
506                                 http_status = r.get('status', 200)
507                                 http_headers = r.get('headers', {
508                                   'content-type': 'application/rss+xml', 
509                                   'content-length':'1'})
510                                 exc_type = r.get("bozo_exception", Exception()).__class__
511                                 if http_status != 304 and not r.get('version', ''):
512                                         if http_status not in [200, 302]: 
513                                                 print >>warn, "W: error %d [%d] %s" % (http_status, feednum, f.url)
514
515                                         elif contains(http_headers.get('content-type', 'rss'), 'html'):
516                                                 print >>warn, "W: looks like HTML [%d] %s"  % (feednum, f.url)
517
518                                         elif http_headers.get('content-length', '1') == '0':
519                                                 print >>warn, "W: empty page [%d] %s" % (feednum, f.url)
520
521                                         elif hasattr(socket, 'timeout') and exc_type == socket.timeout:
522                                                 print >>warn, "W: timed out on [%d] %s" % (feednum, f.url)
523                                         
524                                         elif exc_type == IOError:
525                                                 print >>warn, 'W: "%s" [%d] %s' % (r.bozo_exception, feednum, f.url)
526                                         
527                                         elif hasattr(feedparser, 'zlib') and exc_type == feedparser.zlib.error:
528                                                 print >>warn, "W: broken compression [%d] %s" % (feednum, f.url)
529                                         
530                                         elif exc_type in socket_errors:
531                                                 exc_reason = r.bozo_exception.args[1]
532                                                 print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
533
534                                         elif exc_type == urllib2.URLError:
535                                                 if r.bozo_exception.reason.__class__ in socket_errors:
536                                                         exc_reason = r.bozo_exception.reason.args[1]
537                                                 else:
538                                                         exc_reason = r.bozo_exception.reason
539                                                 print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
540                                         
541                                         elif exc_type == AttributeError:
542                                                 print >>warn, "W: %s [%d] %s" % (r.bozo_exception, feednum, f.url)
543                                         
544                                         elif exc_type == KeyboardInterrupt:
545                                                 raise r.bozo_exception
546                                                 
547                                         elif r.bozo:
548                                                 print >>warn, 'E: error in [%d] "%s" feed (%s)' % (feednum, f.url, r.get("bozo_exception", "can't process"))
549
550                                         else:
551                                                 print >>warn, "=== SEND THE FOLLOWING TO rss2email@aaronsw.com ==="
552                                                 print >>warn, "E:", r.get("bozo_exception", "can't process"), f.url
553                                                 print >>warn, r
554                                                 print >>warn, "rss2email", __version__
555                                                 print >>warn, "feedparser", feedparser.__version__
556                                                 print >>warn, "html2text", h2t.__version__
557                                                 print >>warn, "Python", sys.version
558                                                 print >>warn, "=== END HERE ==="
559                                         continue
560                                 
561                                 r.entries.reverse()
562                                 
563                                 for entry in r.entries:
564                                         id = getID(entry)
565                                         
566                                         # If TRUST_GUID isn't set, we get back hashes of the content.
567                                         # Instead of letting these run wild, we put them in context
568                                         # by associating them with the actual ID (if it exists).
569                                         
570                                         frameid = entry.get('id', id)
571                                         
572                                         # If this item's ID is in our database
573                                         # then it's already been sent
574                                         # and we don't need to do anything more.
575                                         
576                                         if f.seen.has_key(frameid) and f.seen[frameid] == id: continue
577
578                                         if not (f.to or default_to):
579                                                 print "No default email address defined. Please run 'r2e email emailaddress'"
580                                                 print "Ignoring feed %s" % f.url
581                                                 break
582                                         
583                                         if 'title_detail' in entry and entry.title_detail:
584                                                 title = entry.title_detail.value
585                                                 if contains(entry.title_detail.type, 'html'):
586                                                         title = html2text(title)
587                                         else:
588                                                 title = getContent(entry)[:70]
589
590                                         title = title.replace("\n", " ").strip()
591                                         
592                                         datetime = time.gmtime()
593
594                                         if DATE_HEADER:
595                                                 for datetype in DATE_HEADER_ORDER:
596                                                         kind = datetype+"_parsed"
597                                                         if kind in entry and entry[kind]: datetime = entry[kind]
598                                                 
599                                         link = entry.get('link', "")
600                                         
601                                         from_addr = getEmail(r.feed, entry)
602                                         
603                                         name = getName(r, entry)
604                                         fromhdr = '"'+ name + '" <' + from_addr + ">"
605                                         tohdr = (f.to or default_to)
606                                         subjecthdr = title
607                                         datehdr = time.strftime("%a, %d %b %Y %H:%M:%S -0000", datetime)
608                                         useragenthdr = "rss2email"
609                                         extraheaders = {'Date': datehdr, 'User-Agent': useragenthdr}
610                                         if BONUS_HEADER != '':
611                                                 for hdr in BONUS_HEADER.strip().splitlines():
612                                                         pos = hdr.strip().find(':')
613                                                         if pos > 0:
614                                                                 extraheaders[hdr[:pos]] = hdr[pos+1:].strip()
615                                                         else:
616                                                                 print >>warn, "W: malformed BONUS HEADER", BONUS_HEADER 
617                                         
618                                         entrycontent = getContent(entry, HTMLOK=HTML_MAIL)
619                                         contenttype = 'plain'
620                                         content = ''
621                                         if USE_CSS_STYLING and HTML_MAIL:
622                                                 contenttype = 'html'
623                                                 content = "<html>\n" 
624                                                 content += '<head><style><!--' + STYLE_SHEET + '//--></style></head>\n'
625                                                 content += '<body>\n'
626                                                 content += '<div id="entry">\n'
627                                                 content += '<h1'
628                                                 content += ' class="header"'
629                                                 content += '><a href="'+link+'">'+subjecthdr+'</a></h1>\n\n'
630                                                 if ishtml(entrycontent):
631                                                         body = entrycontent[1].strip()
632                                                 else:
633                                                         body = entrycontent.strip()
634                                                 if body != '':  
635                                                         content += '<div id="body"><table><tr><td>\n' + body + '</td></tr></table></div>\n'
636                                                 content += '\n<p class="footer">URL: <a href="'+link+'">'+link+'</a>'
637                                                 if hasattr(entry,'enclosures'):
638                                                         for enclosure in entry.enclosures:
639                                                                 if enclosure.url != "":
640                                                                         content += ('<br/>Enclosure: <a href="'+unu(enclosure.url)+'">'+unu(enclosure.url)+"</a>\n")
641                                                 content += '</p></div>\n'
642                                                 content += "\n\n</body></html>"
643                                         else:   
644                                                 if ishtml(entrycontent):
645                                                         contenttype = 'html'
646                                                         content = "<html>\n" 
647                                                         content = ("<html><body>\n\n" + 
648                                                                    '<h1><a href="'+link+'">'+subjecthdr+'</a></h1>\n\n' +
649                                                                    entrycontent[1].strip() + # drop type tag (HACK: bad abstraction)
650                                                                    '<p>URL: <a href="'+link+'">'+link+'</a></p>' )
651                                                                    
652                                                         if hasattr(entry,'enclosures'):
653                                                                 for enclosure in entry.enclosures:
654                                                                         if enclosure.url != "":
655                                                                                 content += ('Enclosure: <a href="'+unu(enclosure.url)+'">'+unu(enclosure.url)+"</a><br/>\n")
656                                                         
657                                                         content += ("\n</body></html>")
658                                                 else:
659                                                         content = entrycontent.strip() + "\n\nURL: "+link
660                                                         if hasattr(entry,'enclosures'):
661                                                                 for enclosure in entry.enclosures:
662                                                                         if enclosure.url != "":
663                                                                                 content += ('\nEnclosure: '+unu(enclosure.url)+"\n")
664
665                                         smtpserver = send(fromhdr, tohdr, subjecthdr, content, contenttype, extraheaders, smtpserver)
666                         
667                                         f.seen[frameid] = id
668                                         
669                                 f.etag, f.modified = r.get('etag', None), r.get('modified', None)
670                         except (KeyboardInterrupt, SystemExit):
671                                 raise
672                         except:
673                                 print >>warn, "=== SEND THE FOLLOWING TO rss2email@aaronsw.com ==="
674                                 print >>warn, "E: could not parse", f.url
675                                 traceback.print_exc(file=warn)
676                                 print >>warn, "rss2email", __version__
677                                 print >>warn, "feedparser", feedparser.__version__
678                                 print >>warn, "html2text", h2t.__version__
679                                 print >>warn, "Python", sys.version
680                                 print >>warn, "=== END HERE ==="
681                                 continue
682
683         finally:                
684                 unlock(feeds, feedfileObject)
685                 if smtpserver:
686                         smtpserver.quit()
687
688 def list():
689         feeds, feedfileObject = load(lock=0)
690         default_to = ""
691         
692         if feeds and isstr(feeds[0]):
693                 default_to = feeds[0]; ifeeds = feeds[1:]; i=1
694                 print "default email:", default_to
695         else: ifeeds = feeds; i = 0
696         for f in ifeeds:
697                 print `i`+':', f.url, '('+(f.to or ('default: '+default_to))+')'
698                 if not (f.to or default_to):
699                         print "   W: Please define a default address with 'r2e email emailaddress'"
700                 i+= 1
701
702 def delete(n):
703         feeds, feedfileObject = load()
704         if (n == 0) and (feeds and isstr(feeds[0])):
705                 print >>warn, "W: ID has to be equal to or higher than 1"
706         elif n >= len(feeds):
707                 print >>warn, "W: no such feed"
708         else:
709                 print >>warn, "W: deleting feed %s" % feeds[n].url
710                 feeds = feeds[:n] + feeds[n+1:]
711                 if n != len(feeds):
712                         print >>warn, "W: feed IDs have changed, list before deleting again"
713         unlock(feeds, feedfileObject)
714         
715 def email(addr):
716         feeds, feedfileObject = load()
717         if feeds and isstr(feeds[0]): feeds[0] = addr
718         else: feeds = [addr] + feeds
719         unlock(feeds, feedfileObject)
720
721 if __name__ == '__main__':
722         args = sys.argv
723         try:
724                 if len(args) < 3: raise InputError, "insufficient args"
725                 feedfile, action, args = args[1], args[2], args[3:]
726                 
727                 if action == "run": 
728                         if args and args[0] == "--no-send":
729                                 def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
730                                         if VERBOSE: print 'Not sending:', unu(subject)
731
732                         if args and args[-1].isdigit(): run(int(args[-1]))
733                         else: run()
734
735                 elif action == "email":
736                         if not args:
737                                 raise InputError, "Action '%s' requires an argument" % action
738                         else:
739                                 email(args[0])
740
741                 elif action == "add": add(*args)
742
743                 elif action == "new": 
744                         if len(args) == 1: d = [args[0]]
745                         else: d = []
746                         pickle.dump(d, open(feedfile, 'w'))
747
748                 elif action == "list": list()
749
750                 elif action in ("help", "--help", "-h"): print __doc__
751
752                 elif action == "delete":
753                         if not args:
754                                 raise InputError, "Action '%s' requires an argument" % action
755                         elif args[0].isdigit():
756                                 delete(int(args[0]))
757                         else:
758                                 raise InputError, "Action '%s' requires a number as its argument" % action
759
760                 else:
761                         raise InputError, "Invalid action"
762                 
763         except InputError, e:
764                 print "E:", e
765                 print
766                 print __doc__
767