Bump to version 2.65. v2.65
authorLindsey Smith <lindsey@allthingsrss.com>
Mon, 5 Jan 2009 12:00:00 +0000 (12:00 +0000)
committerW. Trevor King <wking@tremily.us>
Thu, 4 Oct 2012 11:05:44 +0000 (07:05 -0400)
rss2email.py [new file with mode: 0644]

diff --git a/rss2email.py b/rss2email.py
new file mode 100644 (file)
index 0000000..1521270
--- /dev/null
@@ -0,0 +1,782 @@
+#!/usr/bin/python
+"""rss2email: get RSS feeds emailed to you
+http://rss2email.infogami.com
+
+Usage:
+  new [emailaddress] (create new feedfile)
+  email newemailaddress (update default email)
+  run [--no-send] [num]
+  add feedurl [emailaddress]
+  list
+  delete n
+"""
+__version__ = "2.65"
+__author__ = "Lindsey Smith (rss2email@aaronsw.com)"
+__copyright__ = "(C) 2004 Aaron Swartz. GNU GPL 2 or 3."
+___contributors__ = ["Dean Jackson", "Brian Lalor", "Joey Hess", 
+                     "Matej Cepl", "Martin 'Joey' Schulze", 
+                     "Marcel Ackermann (http://www.DreamFlasher.de)", 
+                     "Lindsey Smith", "Aaron Swartz (original author)" ]
+
+### Vaguely Customizable Options ###
+
+# The email address messages are from by default:
+DEFAULT_FROM = "bozo@dev.null.invalid"
+
+# 1: Send text/html messages when possible.
+# 0: Convert HTML to plain text.
+HTML_MAIL = 0
+
+# 1: Only use the DEFAULT_FROM address.
+# 0: Use the email address specified by the feed, when possible.
+FORCE_FROM = 0
+
+# 1: Receive one email per post.
+# 0: Receive an email every time a post changes.
+TRUST_GUID = 1
+
+# 1: Generate Date header based on item's date, when possible.
+# 0: Generate Date header based on time sent.
+DATE_HEADER = 0
+
+# A tuple consisting of some combination of
+# ('issued', 'created', 'modified', 'expired')
+# expressing ordered list of preference in dates 
+# to use for the Date header of the email.
+DATE_HEADER_ORDER = ('modified', 'issued', 'created')
+
+# 1: Apply Q-P conversion (required for some MUAs).
+# 0: Send message in 8-bits.
+# http://cr.yp.to/smtp/8bitmime.html
+#DEPRECATED 
+QP_REQUIRED = 0
+#DEPRECATED 
+       
+# 1: Name feeds as they're being processed.
+# 0: Keep quiet.
+VERBOSE = 0
+
+# 1: Use the publisher's email if you can't find the author's.
+# 0: Just use the DEFAULT_FROM email instead.
+USE_PUBLISHER_EMAIL = 0
+
+# 1: Use SMTP_SERVER to send mail.
+# 0: Call /usr/sbin/sendmail to send mail.
+SMTP_SEND = 0
+
+SMTP_SERVER = "smtp.yourisp.net:25"
+AUTHREQUIRED = 0 # if you need to use SMTP AUTH set to 1
+SMTP_USER = 'username'  # for SMTP AUTH, set SMTP username here
+SMTP_PASS = 'password'  # for SMTP AUTH, set SMTP password here
+
+# Set this to add a bonus header to all emails (start with '\n').
+BONUS_HEADER = ''
+# Example: BONUS_HEADER = '\nApproved: joe@bob.org'
+
+# Set this to override From addresses. Keys are feed URLs, values are new titles.
+OVERRIDE_FROM = {}
+
+# Set this to override the timeout (in seconds) for feed server response
+FEED_TIMEOUT = 60
+
+# Optional CSS styling
+USE_CSS_STYLING = 0
+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; }'
+
+# If you have an HTTP Proxy set this in the format 'http://your.proxy.here:8080/'
+PROXY=""
+
+# To most correctly encode emails with international characters, we iterate through the list below and use the first character set that works
+# Eventually (and theoretically) ISO-8859-1 and UTF-8 are our catch-all failsafes
+CHARSET_LIST='US-ASCII', 'BIG5', 'ISO-2022-JP', 'ISO-8859-1', 'UTF-8'
+
+from email.MIMEText import MIMEText
+from email.Header import Header
+from email.Utils import parseaddr, formataddr
+                        
+# Note: You can also override the send function.
+
+def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
+       """Send an email.
+       
+       All arguments should be Unicode strings (plain ASCII works as well).
+       
+       Only the real name part of sender and recipient addresses may contain
+       non-ASCII characters.
+       
+       The email will be properly MIME encoded and delivered though SMTP to
+       localhost port 25.  This is easy to change if you want something different.
+       
+       The charset of the email will be the first one out of the list
+       that can represent all the characters occurring in the email.
+       """
+
+       # Header class is smart enough to try US-ASCII, then the charset we
+       # provide, then fall back to UTF-8.
+       header_charset = 'ISO-8859-1'
+       
+       # We must choose the body charset manually
+       for body_charset in CHARSET_LIST:
+           try:
+               body.encode(body_charset)
+           except (UnicodeError, LookupError):
+               pass
+           else:
+               break
+
+       # Split real name (which is optional) and email address parts
+       sender_name, sender_addr = parseaddr(sender)
+       recipient_name, recipient_addr = parseaddr(recipient)
+       
+       # We must always pass Unicode strings to Header, otherwise it will
+       # use RFC 2047 encoding even on plain ASCII strings.
+       sender_name = str(Header(unicode(sender_name), header_charset))
+       recipient_name = str(Header(unicode(recipient_name), header_charset))
+       
+       # Make sure email addresses do not contain non-ASCII characters
+       sender_addr = sender_addr.encode('ascii')
+       recipient_addr = recipient_addr.encode('ascii')
+       
+       # Create the message ('plain' stands for Content-Type: text/plain)
+       msg = MIMEText(body.encode(body_charset), contenttype, body_charset)
+       msg['To'] = formataddr((recipient_name, recipient_addr))
+       msg['Subject'] = Header(unicode(subject), header_charset)
+       for hdr in extraheaders.keys():
+               try:
+                       msg[hdr] = Header(unicode(extraheaders[hdr], header_charset))
+               except:
+                       msg[hdr] = Header(extraheaders[hdr])
+               
+       fromhdr = formataddr((sender_name, sender_addr))
+       msg['From'] = fromhdr
+               
+       msg_as_string = msg.as_string()
+#DEPRECATED    if QP_REQUIRED:
+#DEPRECATED            ins, outs = SIO(msg_as_string), SIO()
+#DEPRECATED            mimify.mimify(ins, outs)
+#DEPRECATED            msg_as_string = outs.getvalue()
+               
+       if SMTP_SEND:
+               if not smtpserver: 
+                       import smtplib
+                       
+                       try:
+                               smtpserver = smtplib.SMTP(SMTP_SERVER)
+                       except KeyboardInterrupt:
+                               raise
+                       except Exception, e:
+                               print >>warn, ""
+                               print >>warn, ('Fatal error: could not connect to mail server "%s"' % SMTP_SERVER)
+                               if hasattr(e, 'reason'):
+                                       print >>warn, "Reason:", e.reason
+                               sys.exit(1)
+                                       
+                       if AUTHREQUIRED:
+                               try:
+                                       smtpserver.ehlo()
+                                       smtpserver.starttls()
+                                       smtpserver.ehlo()
+                                       smtpserver.login(SMTP_USER, SMTP_PASS)
+                               except KeyboardInterrupt:
+                                       raise
+                               except Exception, e:
+                                       print >>warn, ""
+                                       print >>warn, ('Fatal error: could not authenticate with mail server "%s" as user "%s"' % (SMTP_SERVER, SMTP_USER))
+                                       if hasattr(e, 'reason'):
+                                               print >>warn, "Reason:", e.reason
+                                       sys.exit(1)
+                                       
+               smtpserver.sendmail(sender, recipient, msg_as_string)
+               return smtpserver
+
+       else:
+               try:
+                       p = subprocess.Popen(["/usr/sbin/sendmail", recipient], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+                       p.communicate(msg_as_string)
+                       status = p.returncode
+                       assert status != None, "just a sanity check"
+                       if status != 0:
+                               print >>warn, ""
+                               print >>warn, ('Fatal error: sendmail exited with code %s' % status)
+                               sys.exit(1)
+               except:
+                       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:
+# 1: Use SMTP_SERVER to send mail.
+# 0: Call /usr/sbin/sendmail to send mail.
+SMTP_SEND = 0
+
+SMTP_SERVER = "smtp.yourisp.net:25"
+AUTHREQUIRED = 0 # if you need to use SMTP AUTH set to 1
+SMTP_USER = 'username'  # for SMTP AUTH, set SMTP username here
+SMTP_PASS = 'password'  # for SMTP AUTH, set SMTP password here
+'''
+                       sys.exit(1)
+               return None
+
+## html2text options ##
+
+# Use Unicode characters instead of their ascii psuedo-replacements
+UNICODE_SNOB = 0
+
+# Put the links after each paragraph instead of at the end.
+LINKS_EACH_PARAGRAPH = 0
+
+# Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
+BODY_WIDTH = 0
+
+### Load the Options ###
+
+# Read options from config file if present.
+import sys
+sys.path.insert(0,".")
+try:
+       from config import *
+except:
+       pass
+
+warn = sys.stderr
+       
+if QP_REQUIRED:
+       print >>warn, "QP_REQUIRED has been deprecated in rss2email."
+
+### Import Modules ###
+
+import cPickle as pickle, time, os, traceback, urllib2, sys, types, subprocess
+hash = ()
+try:
+       import hashlib
+       hash = hashlib.md5
+except ImportError:
+       import md5
+       hash = md5.new
+
+unix = 0
+try:
+       import fcntl
+# A pox on SunOS file locking methods  
+       if (sys.platform.find('sunos') == -1): 
+               unix = 1
+except:
+       pass
+               
+import socket; socket_errors = []
+for e in ['error', 'gaierror']:
+       if hasattr(socket, e): socket_errors.append(getattr(socket, e))
+
+#DEPRECATED import mimify 
+#DEPRECATED from StringIO import StringIO as SIO 
+#DEPRECATED mimify.CHARSET = 'utf-8'
+
+import feedparser
+feedparser.USER_AGENT = "rss2email/"+__version__+ " +http://www.aaronsw.com/2002/rss2email/"
+
+import html2text as h2t
+
+h2t.UNICODE_SNOB = UNICODE_SNOB
+h2t.LINKS_EACH_PARAGRAPH = LINKS_EACH_PARAGRAPH
+h2t.BODY_WIDTH = BODY_WIDTH
+html2text = h2t.html2text
+
+### Utility Functions ###
+
+import threading
+class TimeoutError(Exception): pass
+
+class InputError(Exception): pass
+
+def timelimit(timeout, function):
+#    def internal(function):
+        def internal2(*args, **kw):
+            """
+            from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878
+            """
+            class Calculator(threading.Thread):
+                def __init__(self):
+                    threading.Thread.__init__(self)
+                    self.result = None
+                    self.error = None
+                
+                def run(self):
+                    try:
+                        self.result = function(*args, **kw)
+                    except:
+                        self.error = sys.exc_info()
+            
+            c = Calculator()
+            c.setDaemon(True) # don't hold up exiting
+            c.start()
+            c.join(timeout)
+            if c.isAlive():
+                raise TimeoutError
+            if c.error:
+                raise c.error[0], c.error[1]
+            return c.result
+        return internal2
+#    return internal
+    
+
+def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u''))
+def ishtml(t): return type(t) is type(())
+def contains(a,b): return a.find(b) != -1
+def unu(s): # I / freakin' hate / that unicode
+       if type(s) is types.UnicodeType: return s.encode('utf-8')
+       else: return s
+
+### Parsing Utilities ###
+
+def getContent(entry, HTMLOK=0):
+       """Select the best content from an entry, deHTMLizing if necessary.
+       If raw HTML is best, an ('HTML', best) tuple is returned. """
+       
+       # How this works:
+       #  * We have a bunch of potential contents. 
+       #  * We go thru looking for our first choice. 
+       #    (HTML or text, depending on HTMLOK)
+       #  * If that doesn't work, we go thru looking for our second choice.
+       #  * If that still doesn't work, we just take the first one.
+       #
+       # Possible future improvement:
+       #  * Instead of just taking the first one
+       #    pick the one in the "best" language.
+       #  * HACK: hardcoded HTMLOK, should take a tuple of media types
+       
+       conts = entry.get('content', [])
+       
+       if entry.get('summary_detail', {}):
+               conts += [entry.summary_detail]
+       
+       if conts:
+               if HTMLOK:
+                       for c in conts:
+                               if contains(c.type, 'html'): return ('HTML', c.value)
+       
+               if not HTMLOK: # Only need to convert to text if HTML isn't OK
+                       for c in conts:
+                               if contains(c.type, 'html'):
+                                       return html2text(c.value)
+               
+               for c in conts:
+                       if c.type == 'text/plain': return c.value
+       
+               return conts[0].value   
+       
+       return ""
+
+def getID(entry):
+       """Get best ID from an entry."""
+       if TRUST_GUID:
+               if 'id' in entry and entry.id: return entry.id
+
+       content = getContent(entry)
+       if content and content != "\n": return hash(unu(content)).hexdigest()
+       if 'link' in entry: return entry.link
+       if 'title' in entry: return hash(unu(entry.title)).hexdigest()
+
+def getName(r, entry):
+       """Get the best name."""
+
+       feed = r.feed
+       if hasattr(r, "url") and r.url in OVERRIDE_FROM.keys():
+               return OVERRIDE_FROM[r.url]
+       
+       name = feed.get('title', '')
+
+       if 'name' in entry.get('author_detail', []): # normally {} but py2.1
+               if entry.author_detail.name:
+                       if name: name += ": "
+                       det=entry.author_detail.name
+                       try:
+                           name +=  entry.author_detail.name
+                       except UnicodeDecodeError:
+                           name +=  unicode(entry.author_detail.name, 'utf-8')
+
+       elif 'name' in feed.get('author_detail', []):
+               if feed.author_detail.name:
+                       if name: name += ", "
+                       name += feed.author_detail.name
+       
+       return name
+
+def getEmail(feed, entry):
+       """Get the best email_address."""
+
+       if FORCE_FROM: return DEFAULT_FROM
+       
+       if 'email' in entry.get('author_detail', []):
+               return entry.author_detail.email
+       
+       if 'email' in feed.get('author_detail', []):
+               return feed.author_detail.email
+               
+       #TODO: contributors
+       
+       if USE_PUBLISHER_EMAIL:
+               if 'email' in feed.get('publisher_detail', []):
+                       return feed.publisher_detail.email
+               
+               if feed.get("errorreportsto", ''):
+                       return feed.errorreportsto
+                       
+       return DEFAULT_FROM
+
+### Simple Database of Feeds ###
+
+class Feed:
+       def __init__(self, url, to):
+               self.url, self.etag, self.modified, self.seen = url, None, None, {}
+               self.to = to            
+
+def load(lock=1):
+       if not os.path.exists(feedfile):
+               print 'Feedfile "%s" does not exist.  If you\'re using r2e for the first time, you' % feedfile
+               print "have to run 'r2e new' first."
+               sys.exit(1)
+       try:
+               feedfileObject = open(feedfile, 'r')
+       except IOError, e:
+               print "Feedfile could not be opened: %s" % e
+               sys.exit(1)
+       feeds = pickle.load(feedfileObject)
+       
+       if lock:
+               locktype = 0
+               if unix:
+                       locktype = fcntl.LOCK_EX
+                       fcntl.flock(feedfileObject.fileno(), locktype)
+               #HACK: to deal with lock caching
+               feedfileObject = open(feedfile, 'r')
+               feeds = pickle.load(feedfileObject)
+               if unix: 
+                       fcntl.flock(feedfileObject.fileno(), locktype)
+
+       return feeds, feedfileObject
+
+def unlock(feeds, feedfileObject):
+       if not unix: 
+               pickle.dump(feeds, open(feedfile, 'w'))
+       else:   
+               pickle.dump(feeds, open(feedfile+'.tmp', 'w'))
+               os.rename(feedfile+'.tmp', feedfile)
+               fcntl.flock(feedfileObject.fileno(), fcntl.LOCK_UN)
+
+#@timelimit(FEED_TIMEOUT)              
+def parse(url, etag, modified):
+       if PROXY == '':
+               return feedparser.parse(url, etag, modified)
+       else:
+               proxy = urllib2.ProxyHandler( {"http":PROXY} )
+               return feedparser.parse(url, etag, modified, handlers = [proxy])        
+       
+               
+### Program Functions ###
+
+def add(*args):
+       if len(args) == 2 and contains(args[1], '@') and not contains(args[1], '://'):
+               urls, to = [args[0]], args[1]
+       else:
+               urls, to = args, None
+       
+       feeds, feedfileObject = load()
+       if (feeds and not isstr(feeds[0]) and to is None) or (not len(feeds) and to is None):
+               print "No email address has been defined. Please run 'r2e email emailaddress' or"
+               print "'r2e add url emailaddress'."
+               sys.exit(1)
+       for url in urls: feeds.append(Feed(url, to))
+       unlock(feeds, feedfileObject)
+
+def run(num=None):
+       feeds, feedfileObject = load()
+       smtpserver = None
+       try:
+               # We store the default to address as the first item in the feeds list.
+               # Here we take it out and save it for later.
+               default_to = ""
+               if feeds and isstr(feeds[0]): default_to = feeds[0]; ifeeds = feeds[1:] 
+               else: ifeeds = feeds
+               
+               if num: ifeeds = [feeds[num]]
+               feednum = 0
+               
+               for f in ifeeds:
+                       try: 
+                               feednum += 1
+                               if VERBOSE: print >>warn, 'I: Processing [%d] "%s"' % (feednum, f.url)
+                               r = {}
+                               try:
+                                       r = timelimit(FEED_TIMEOUT, parse)(f.url, f.etag, f.modified)
+                               except TimeoutError:
+                                       print >>warn, 'W: feed [%d] "%s" timed out' % (feednum, f.url)
+                                       continue
+                               
+                               # Handle various status conditions, as required
+                               if 'status' in r:
+                                       if r.status == 301: f.url = r['url']
+                                       elif r.status == 410:
+                                               print >>warn, "W: feed gone; deleting", f.url
+                                               feeds.remove(f)
+                                               continue
+                               
+                               http_status = r.get('status', 200)
+                               http_headers = r.get('headers', {
+                                 'content-type': 'application/rss+xml', 
+                                 'content-length':'1'})
+                               exc_type = r.get("bozo_exception", Exception()).__class__
+                               if http_status != 304 and not r.get('version', ''):
+                                       if http_status not in [200, 302]: 
+                                               print >>warn, "W: error %d [%d] %s" % (http_status, feednum, f.url)
+
+                                       elif contains(http_headers.get('content-type', 'rss'), 'html'):
+                                               print >>warn, "W: looks like HTML [%d] %s"  % (feednum, f.url)
+
+                                       elif http_headers.get('content-length', '1') == '0':
+                                               print >>warn, "W: empty page [%d] %s" % (feednum, f.url)
+
+                                       elif hasattr(socket, 'timeout') and exc_type == socket.timeout:
+                                               print >>warn, "W: timed out on [%d] %s" % (feednum, f.url)
+                                       
+                                       elif exc_type == IOError:
+                                               print >>warn, 'W: "%s" [%d] %s' % (r.bozo_exception, feednum, f.url)
+                                       
+                                       elif hasattr(feedparser, 'zlib') and exc_type == feedparser.zlib.error:
+                                               print >>warn, "W: broken compression [%d] %s" % (feednum, f.url)
+                                       
+                                       elif exc_type in socket_errors:
+                                               exc_reason = r.bozo_exception.args[1]
+                                               print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
+
+                                       elif exc_type == urllib2.URLError:
+                                               if r.bozo_exception.reason.__class__ in socket_errors:
+                                                       exc_reason = r.bozo_exception.reason.args[1]
+                                               else:
+                                                       exc_reason = r.bozo_exception.reason
+                                               print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
+                                       
+                                       elif exc_type == AttributeError:
+                                               print >>warn, "W: %s [%d] %s" % (r.bozo_exception, feednum, f.url)
+                                       
+                                       elif exc_type == KeyboardInterrupt:
+                                               raise r.bozo_exception
+                                               
+                                       elif r.bozo:
+                                               print >>warn, 'E: error in [%d] "%s" feed (%s)' % (feednum, f.url, r.get("bozo_exception", "can't process"))
+
+                                       else:
+                                               print >>warn, "=== SEND THE FOLLOWING TO rss2email@aaronsw.com ==="
+                                               print >>warn, "E:", r.get("bozo_exception", "can't process"), f.url
+                                               print >>warn, r
+                                               print >>warn, "rss2email", __version__
+                                               print >>warn, "feedparser", feedparser.__version__
+                                               print >>warn, "html2text", h2t.__version__
+                                               print >>warn, "Python", sys.version
+                                               print >>warn, "=== END HERE ==="
+                                       continue
+                               
+                               r.entries.reverse()
+                               
+                               for entry in r.entries:
+                                       id = getID(entry)
+                                       
+                                       # If TRUST_GUID isn't set, we get back hashes of the content.
+                                       # Instead of letting these run wild, we put them in context
+                                       # by associating them with the actual ID (if it exists).
+                                       
+                                       frameid = entry.get('id', id)
+                                       
+                                       # If this item's ID is in our database
+                                       # then it's already been sent
+                                       # and we don't need to do anything more.
+                                       
+                                       if f.seen.has_key(frameid) and f.seen[frameid] == id: continue
+
+                                       if not (f.to or default_to):
+                                               print "No default email address defined. Please run 'r2e email emailaddress'"
+                                               print "Ignoring feed %s" % f.url
+                                               break
+                                       
+                                       if 'title_detail' in entry and entry.title_detail:
+                                               title = entry.title_detail.value
+                                               if contains(entry.title_detail.type, 'html'):
+                                                       title = html2text(title)
+                                       else:
+                                               title = getContent(entry)[:70]
+
+                                       title = title.replace("\n", " ").strip()
+                                       
+                                       datetime = time.gmtime()
+
+                                       if DATE_HEADER:
+                                               for datetype in DATE_HEADER_ORDER:
+                                                       kind = datetype+"_parsed"
+                                                       if kind in entry and entry[kind]: datetime = entry[kind]
+                                               
+                                       link = entry.get('link', "")
+                                       
+                                       from_addr = getEmail(r.feed, entry)
+                                       
+                                       name = getName(r, entry)
+                                       fromhdr = '"'+ name + '" <' + from_addr + ">"
+                                       tohdr = (f.to or default_to)
+                                       subjecthdr = title
+                                       datehdr = time.strftime("%a, %d %b %Y %H:%M:%S -0000", datetime)
+                                       useragenthdr = "rss2email"
+                                       extraheaders = {'Date': datehdr, 'User-Agent': useragenthdr}
+                                       if BONUS_HEADER != '':
+                                               for hdr in BONUS_HEADER.strip().splitlines():
+                                                       pos = hdr.strip().find(':')
+                                                       if pos > 0:
+                                                               extraheaders[hdr[:pos]] = hdr[pos+1:].strip()
+                                                       else:
+                                                               print >>warn, "W: malformed BONUS HEADER", BONUS_HEADER 
+                                       
+                                       entrycontent = getContent(entry, HTMLOK=HTML_MAIL)
+                                       contenttype = 'plain'
+                                       content = ''
+                                       if USE_CSS_STYLING and HTML_MAIL:
+                                               contenttype = 'html'
+                                               content = "<html>\n" 
+                                               content += '<head><style><!--' + STYLE_SHEET + '//--></style></head>\n'
+                                               content += '<body>\n'
+                                               content += '<div id="entry">\n'
+                                               content += '<h1'
+                                               content += ' class="header"'
+                                               content += '><a href="'+link+'">'+subjecthdr+'</a></h1>\n\n'
+                                               if ishtml(entrycontent):
+                                                       body = entrycontent[1].strip()
+                                               else:
+                                                       body = entrycontent.strip()
+                                               if body != '':  
+                                                       content += '<div id="body"><table><tr><td>\n' + body + '</td></tr></table></div>\n'
+                                               content += '\n<p class="footer">URL: <a href="'+link+'">'+link+'</a>'
+                                               if hasattr(entry,'enclosures'):
+                                                       for enclosure in entry.enclosures:
+                                                               if (hasattr(enclosure, 'url') and enclosure.url != ""):
+                                                                       content += ('<br/>Enclosure: <a href="'+unu(enclosure.url)+'">'+unu(enclosure.url)+"</a>\n")
+                                                               if (hasattr(enclosure, 'src') and enclosure.src != ""):
+                                                                       content += ('<br/>Enclosure: <a href="'+unu(enclosure.src)+'">'+unu(enclosure.src)+'</a><br/><img src="'+unu(enclosure.src)+'"\n')
+                                               content += '</p></div>\n'
+                                               content += "\n\n</body></html>"
+                                       else:   
+                                               if ishtml(entrycontent):
+                                                       contenttype = 'html'
+                                                       content = "<html>\n" 
+                                                       content = ("<html><body>\n\n" + 
+                                                                  '<h1><a href="'+link+'">'+subjecthdr+'</a></h1>\n\n' +
+                                                                  entrycontent[1].strip() + # drop type tag (HACK: bad abstraction)
+                                                                  '<p>URL: <a href="'+link+'">'+link+'</a></p>' )
+                                                                  
+                                                       if hasattr(entry,'enclosures'):
+                                                               for enclosure in entry.enclosures:
+                                                                       if enclosure.url != "":
+                                                                               content += ('Enclosure: <a href="'+unu(enclosure.url)+'">'+unu(enclosure.url)+"</a><br/>\n")
+                                                       
+                                                       content += ("\n</body></html>")
+                                               else:
+                                                       content = entrycontent.strip() + "\n\nURL: "+link
+                                                       if hasattr(entry,'enclosures'):
+                                                               for enclosure in entry.enclosures:
+                                                                       if enclosure.url != "":
+                                                                               content += ('\nEnclosure: '+unu(enclosure.url)+"\n")
+
+                                       smtpserver = send(fromhdr, tohdr, subjecthdr, content, contenttype, extraheaders, smtpserver)
+                       
+                                       f.seen[frameid] = id
+                                       
+                               f.etag, f.modified = r.get('etag', None), r.get('modified', None)
+                       except (KeyboardInterrupt, SystemExit):
+                               raise
+                       except:
+                               print >>warn, "=== SEND THE FOLLOWING TO rss2email@aaronsw.com ==="
+                               print >>warn, "E: could not parse", f.url
+                               traceback.print_exc(file=warn)
+                               print >>warn, "rss2email", __version__
+                               print >>warn, "feedparser", feedparser.__version__
+                               print >>warn, "html2text", h2t.__version__
+                               print >>warn, "Python", sys.version
+                               print >>warn, "=== END HERE ==="
+                               continue
+
+       finally:                
+               unlock(feeds, feedfileObject)
+               if smtpserver:
+                       smtpserver.quit()
+
+def list():
+       feeds, feedfileObject = load(lock=0)
+       default_to = ""
+       
+       if feeds and isstr(feeds[0]):
+               default_to = feeds[0]; ifeeds = feeds[1:]; i=1
+               print "default email:", default_to
+       else: ifeeds = feeds; i = 0
+       for f in ifeeds:
+               print `i`+':', f.url, '('+(f.to or ('default: '+default_to))+')'
+               if not (f.to or default_to):
+                       print "   W: Please define a default address with 'r2e email emailaddress'"
+               i+= 1
+
+def delete(n):
+       feeds, feedfileObject = load()
+       if (n == 0) and (feeds and isstr(feeds[0])):
+               print >>warn, "W: ID has to be equal to or higher than 1"
+       elif n >= len(feeds):
+               print >>warn, "W: no such feed"
+       else:
+               print >>warn, "W: deleting feed %s" % feeds[n].url
+               feeds = feeds[:n] + feeds[n+1:]
+               if n != len(feeds):
+                       print >>warn, "W: feed IDs have changed, list before deleting again"
+       unlock(feeds, feedfileObject)
+       
+def email(addr):
+       feeds, feedfileObject = load()
+       if feeds and isstr(feeds[0]): feeds[0] = addr
+       else: feeds = [addr] + feeds
+       unlock(feeds, feedfileObject)
+
+if __name__ == '__main__':
+       args = sys.argv
+       try:
+               if len(args) < 3: raise InputError, "insufficient args"
+               feedfile, action, args = args[1], args[2], args[3:]
+               
+               if action == "run": 
+                       if args and args[0] == "--no-send":
+                               def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
+                                       if VERBOSE: print 'Not sending:', unu(subject)
+
+                       if args and args[-1].isdigit(): run(int(args[-1]))
+                       else: run()
+
+               elif action == "email":
+                       if not args:
+                               raise InputError, "Action '%s' requires an argument" % action
+                       else:
+                               email(args[0])
+
+               elif action == "add": add(*args)
+
+               elif action == "new": 
+                       if len(args) == 1: d = [args[0]]
+                       else: d = []
+                       pickle.dump(d, open(feedfile, 'w'))
+
+               elif action == "list": list()
+
+               elif action in ("help", "--help", "-h"): print __doc__
+
+               elif action == "delete":
+                       if not args:
+                               raise InputError, "Action '%s' requires an argument" % action
+                       elif args[0].isdigit():
+                               delete(int(args[0]))
+                       else:
+                               raise InputError, "Action '%s' requires a number as its argument" % action
+
+               else:
+                       raise InputError, "Invalid action"
+               
+       except InputError, e:
+               print "E:", e
+               print
+               print __doc__
+