2 """rss2email: get RSS feeds emailed to you
3 http://rss2email.infogami.com
6 new [emailaddress] (create new feedfile)
7 email newemailaddress (update default email)
9 add feedurl [emailaddress]
19 __author__ = 'Lindsey Smith (lindsey@allthingsrss.com)'
20 __copyright__ = '(C) 2004 Aaron Swartz. GNU GPL 2 or 3.'
26 "Martin 'Joey' Schulze",
27 'Marcel Ackermann (http://www.DreamFlasher.de)',
28 'Lindsey Smith (maintainer)',
30 'Aaron Swartz (original author)',
33 import collections as _collections
34 import configparser as _configparser
35 from email.mime.text import MIMEText as _MIMEText
36 from email.header import Header as _Header
37 from email.utils import parseaddr as _parseaddr
38 from email.utils import formataddr as _formataddr
39 import hashlib as _hashlib
41 import pickle as _pickle
42 import smtplib as _smtplib
43 import socket as _socket
44 import subprocess as _subprocess
46 import threading as _threading
48 import traceback as _traceback
49 import types as _types
50 import urllib.request as _urllib_request
51 import xml.dom.minidom as _minidom
52 import xml.sax.saxutils as _saxutils
56 import fcntl as _fcntl
57 # A pox on SunOS file locking methods
58 if 'sunos' not in sys.platform:
63 import feedparser as _feedparser
64 import html2text as _html2text
67 _urllib_request.install_opener(_urllib_request.build_opener())
70 class Config (_configparser.ConfigParser):
71 def __init__(self, **kwargs):
72 super(Config, self).__init__(dict_type=_collections.OrderedDict)
77 # setup defaults for feeds that don't customize
78 CONFIG['DEFAULT'] = _collections.OrderedDict((
80 # The email address messages are from by default
81 ('from', 'bozo@dev.null.invalid'),
82 # True: Only use the 'from' address.
83 # False: Use the email address specified by the feed, when possible.
84 ('force-from', str(False)),
85 # True: Use the publisher's email if you can't find the author's.
86 # False: Just use the 'from' email instead.
87 ('use-publisher-email', str(False)),
88 # Only use the feed email address rather than friendly name
90 ('friendly-name', str(True)),
91 # Set this to override From addresses.
92 ('override-from', str(False)),
93 # Set this to default To email addresses.
95 # Set this to override To email addresses.
96 ('override-to', False),
99 # Set an HTTP proxy (e.g. 'http://your.proxy.here:8080/')
101 # Set the timeout (in seconds) for feed server response
102 ('feed-timeout', str(60)),
105 # True: Generate Date header based on item's date, when possible.
106 # False: Generate Date header based on time sent.
107 ('date-header', str(False)),
108 # A comma-delimited list of some combination of
109 # ('issued', 'created', 'modified', 'expired')
110 # expressing ordered list of preference in dates
111 # to use for the Date header of the email.
112 ('date-header-order', 'modified, issued, created, expired'),
113 # Set this to add a bonus header to all emails (start with '\n').
114 # Example: bonus-header = '\nApproved: joe@bob.org'
115 ('bonus-header', ''),
116 # True: Receive one email per post.
117 # False: Receive an email every time a post changes.
118 ('trust-guid', str(True)),
119 # To most correctly encode emails with international
120 # characters, we iterate through the list below and use the
121 # first character set that works Eventually (and
122 # theoretically) UTF-8 is our catch-all failsafe.
123 ('charsets', 'US-ASCII, BIG5, ISO-2022-JP, ISO-8859-1, UTF-8'),
125 # True: Send text/html messages when possible.
126 # False: Convert HTML to plain text.
127 ('html-mail', str(False)),
128 # Optional CSS styling
129 ('use-css', str(False)),
132 ' font: 18pt Georgia, "Times New Roman";\n'
135 ' font: 12pt Arial;\n'
138 ' font: 12pt Arial;\n'
139 ' font-weight: bold;\n'
143 ' font-family: monospace;\n'
146 ' background: #e0ecff;\n'
147 ' border-bottom: solid 4px #c3d9ff;\n'
149 ' margin-top: 0px;\n'
153 ' font-size: 20px;\n'
154 ' text-decoration: none;\n'
157 ' background: #c3d9ff;\n'
158 ' border-top: solid 4px #c3d9ff;\n'
160 ' margin-bottom: 0px;\n'
163 ' border: solid 4px #c3d9ff;\n'
166 ' margin-left: 5px;\n'
167 ' margin-right: 5px;\n'
170 # Use Unicode characters instead of their ascii psuedo-replacements
171 ('unicode-snob': str(False)),
172 # Put the links after each paragraph instead of at the end.
173 ('link-after-each-paragraph', str(False)),
174 # Wrap long lines at position. 0 for no wrapping.
175 ('body-width', str(0)),
178 # True: Use SMTP_SERVER to send mail.
179 # False: Call /usr/sbin/sendmail to send mail.
180 ('use-smtp', str(False)),
181 ('smtp-server', 'smtp.yourisp.net:25'),
182 ('smtp-auth', str(False)), # set to True to use SMTP AUTH
183 ('smtp-username', 'username'), # username for SMTP AUTH
184 ('smtp-password', 'password'), # password for SMTP AUTH
185 ('smtp-ssl', str(False)), # Connect to the SMTP server using SSL
188 # Verbosity (one of 'error', 'warning', 'info', or 'debug').
189 ('verbose', 'warning'),
193 def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
196 All arguments should be Unicode strings (plain ASCII works as well).
198 Only the real name part of sender and recipient addresses may contain
199 non-ASCII characters.
201 The email will be properly MIME encoded and delivered though SMTP to
202 localhost port 25. This is easy to change if you want something different.
204 The charset of the email will be the first one out of the list
205 that can represent all the characters occurring in the email.
208 # Header class is smart enough to try US-ASCII, then the charset we
209 # provide, then fall back to UTF-8.
210 header_charset = 'ISO-8859-1'
212 # We must choose the body charset manually
213 for body_charset in CHARSET_LIST:
215 body.encode(body_charset)
216 except (UnicodeError, LookupError):
221 # Split real name (which is optional) and email address parts
222 sender_name, sender_addr = parseaddr(sender)
223 recipient_name, recipient_addr = parseaddr(recipient)
225 # We must always pass Unicode strings to Header, otherwise it will
226 # use RFC 2047 encoding even on plain ASCII strings.
227 sender_name = str(Header(unicode(sender_name), header_charset))
228 recipient_name = str(Header(unicode(recipient_name), header_charset))
230 # Make sure email addresses do not contain non-ASCII characters
231 sender_addr = sender_addr.encode('ascii')
232 recipient_addr = recipient_addr.encode('ascii')
234 # Create the message ('plain' stands for Content-Type: text/plain)
235 msg = MIMEText(body.encode(body_charset), contenttype, body_charset)
236 msg['To'] = formataddr((recipient_name, recipient_addr))
237 msg['Subject'] = Header(unicode(subject), header_charset)
238 for hdr in extraheaders.keys():
240 msg[hdr] = Header(unicode(extraheaders[hdr], header_charset))
242 msg[hdr] = Header(extraheaders[hdr])
244 fromhdr = formataddr((sender_name, sender_addr))
245 msg['From'] = fromhdr
247 msg_as_string = msg.as_string()
248 #DEPRECATED if QP_REQUIRED:
249 #DEPRECATED ins, outs = SIO(msg_as_string), SIO()
250 #DEPRECATED mimify.mimify(ins, outs)
251 #DEPRECATED msg_as_string = outs.getvalue()
257 smtpserver = smtplib.SMTP_SSL()
259 smtpserver = smtplib.SMTP()
260 smtpserver.connect(SMTP_SERVER)
261 except KeyboardInterrupt:
265 print >>warn, ('Fatal error: could not connect to mail server "%s"' % SMTP_SERVER)
266 print >>warn, ('Check your config.py file to confirm that SMTP_SERVER and other mail server settings are configured properly')
267 if hasattr(e, 'reason'):
268 print >>warn, "Reason:", e.reason
274 if not SMTP_SSL: smtpserver.starttls()
276 smtpserver.login(SMTP_USER, SMTP_PASS)
277 except KeyboardInterrupt:
281 print >>warn, ('Fatal error: could not authenticate with mail server "%s" as user "%s"' % (SMTP_SERVER, SMTP_USER))
282 print >>warn, ('Check your config.py file to confirm that SMTP_SERVER and other mail server settings are configured properly')
283 if hasattr(e, 'reason'):
284 print >>warn, "Reason:", e.reason
287 smtpserver.sendmail(sender, recipient, msg_as_string)
292 p = subprocess.Popen(["/usr/sbin/sendmail", recipient], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
293 p.communicate(msg_as_string)
294 status = p.returncode
295 assert status != None, "just a sanity check"
298 print >>warn, ('Fatal error: sendmail exited with code %s' % status)
301 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:
302 # 1: Use SMTP_SERVER to send mail.
303 # 0: Call /usr/sbin/sendmail to send mail.
306 SMTP_SERVER = "smtp.yourisp.net:25"
307 AUTHREQUIRED = 0 # if you need to use SMTP AUTH set to 1
308 SMTP_USER = 'username' # for SMTP AUTH, set SMTP username here
309 SMTP_PASS = 'password' # for SMTP AUTH, set SMTP password here
314 ### Load the Options ###
316 # Read options from config file if present.
317 sys.path.insert(0,".")
326 print >>warn, "QP_REQUIRED has been deprecated in rss2email."
329 for e in ['error', 'gaierror']:
330 if hasattr(socket, e): socket_errors.append(getattr(socket, e))
332 feedparser.USER_AGENT = "rss2email/"+__version__+ " +http://www.allthingsrss.com/rss2email/"
335 h2t.UNICODE_SNOB = UNICODE_SNOB
336 h2t.LINKS_EACH_PARAGRAPH = LINKS_EACH_PARAGRAPH
337 h2t.BODY_WIDTH = BODY_WIDTH
338 html2text = h2t.html2text
340 ### Utility Functions ###
342 class TimeoutError(Exception): pass
344 class InputError(Exception): pass
346 def timelimit(timeout, function):
347 # def internal(function):
348 def internal2(*args, **kw):
350 from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878
352 class Calculator(threading.Thread):
354 threading.Thread.__init__(self)
360 self.result = function(*args, **kw)
362 self.error = sys.exc_info()
365 c.setDaemon(True) # don't hold up exiting
371 raise c.error[0], c.error[1]
377 def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u''))
378 def ishtml(t): return type(t) is type(())
379 def contains(a,b): return a.find(b) != -1
380 def unu(s): # I / freakin' hate / that unicode
381 if type(s) is types.UnicodeType: return s.encode('utf-8')
384 ### Parsing Utilities ###
386 def getContent(entry, HTMLOK=0):
387 """Select the best content from an entry, deHTMLizing if necessary.
388 If raw HTML is best, an ('HTML', best) tuple is returned. """
391 # * We have a bunch of potential contents.
392 # * We go thru looking for our first choice.
393 # (HTML or text, depending on HTMLOK)
394 # * If that doesn't work, we go thru looking for our second choice.
395 # * If that still doesn't work, we just take the first one.
397 # Possible future improvement:
398 # * Instead of just taking the first one
399 # pick the one in the "best" language.
400 # * HACK: hardcoded HTMLOK, should take a tuple of media types
402 conts = entry.get('content', [])
404 if entry.get('summary_detail', {}):
405 conts += [entry.summary_detail]
410 if contains(c.type, 'html'): return ('HTML', c.value)
412 if not HTMLOK: # Only need to convert to text if HTML isn't OK
414 if contains(c.type, 'html'):
415 return html2text(c.value)
418 if c.type == 'text/plain': return c.value
420 return conts[0].value
425 """Get best ID from an entry."""
427 if 'id' in entry and entry.id:
428 # Newer versions of feedparser could return a dictionary
429 if type(entry.id) is DictType:
430 return entry.id.values()[0]
434 content = getContent(entry)
435 if content and content != "\n": return hash(unu(content)).hexdigest()
436 if 'link' in entry: return entry.link
437 if 'title' in entry: return hash(unu(entry.title)).hexdigest()
439 def getName(r, entry):
440 """Get the best name."""
442 if NO_FRIENDLY_NAME: return ''
445 if hasattr(r, "url") and r.url in OVERRIDE_FROM.keys():
446 return OVERRIDE_FROM[r.url]
448 name = feed.get('title', '')
450 if 'name' in entry.get('author_detail', []): # normally {} but py2.1
451 if entry.author_detail.name:
452 if name: name += ": "
453 det=entry.author_detail.name
455 name += entry.author_detail.name
456 except UnicodeDecodeError:
457 name += unicode(entry.author_detail.name, 'utf-8')
459 elif 'name' in feed.get('author_detail', []):
460 if feed.author_detail.name:
461 if name: name += ", "
462 name += feed.author_detail.name
466 def validateEmail(email, planb):
467 """Do a basic quality check on email address, but return planb if email doesn't appear to be well-formed"""
468 email_parts = email.split('@')
469 if len(email_parts) != 2:
473 def getEmail(r, entry):
474 """Get the best email_address. If the best guess isn't well-formed (something@somthing.com), use DEFAULT_FROM instead"""
478 if FORCE_FROM: return DEFAULT_FROM
480 if hasattr(r, "url") and r.url in OVERRIDE_EMAIL.keys():
481 return validateEmail(OVERRIDE_EMAIL[r.url], DEFAULT_FROM)
483 if 'email' in entry.get('author_detail', []):
484 return validateEmail(entry.author_detail.email, DEFAULT_FROM)
486 if 'email' in feed.get('author_detail', []):
487 return validateEmail(feed.author_detail.email, DEFAULT_FROM)
489 if USE_PUBLISHER_EMAIL:
490 if 'email' in feed.get('publisher_detail', []):
491 return validateEmail(feed.publisher_detail.email, DEFAULT_FROM)
493 if feed.get("errorreportsto", ''):
494 return validateEmail(feed.errorreportsto, DEFAULT_FROM)
496 if hasattr(r, "url") and r.url in DEFAULT_EMAIL.keys():
497 return DEFAULT_EMAIL[r.url]
500 ### Simple Database of Feeds ###
503 def __init__(self, url, to):
504 self.url, self.etag, self.modified, self.seen = url, None, None, {}
509 if not os.path.exists(feedfile):
510 print 'Feedfile "%s" does not exist. If you\'re using r2e for the first time, you' % feedfile
511 print "have to run 'r2e new' first."
514 feedfileObject = open(feedfile, 'r')
516 print "Feedfile could not be opened: %s" % e
518 feeds = pickle.load(feedfileObject)
523 locktype = fcntl.LOCK_EX
524 fcntl.flock(feedfileObject.fileno(), locktype)
525 #HACK: to deal with lock caching
526 feedfileObject = open(feedfile, 'r')
527 feeds = pickle.load(feedfileObject)
529 fcntl.flock(feedfileObject.fileno(), locktype)
531 for feed in feeds[1:]:
532 if not hasattr(feed, 'active'):
535 return feeds, feedfileObject
537 def unlock(feeds, feedfileObject):
539 pickle.dump(feeds, open(feedfile, 'w'))
541 fd = open(feedfile+'.tmp', 'w')
542 pickle.dump(feeds, fd)
544 os.fsync(fd.fileno())
546 os.rename(feedfile+'.tmp', feedfile)
547 fcntl.flock(feedfileObject.fileno(), fcntl.LOCK_UN)
549 #@timelimit(FEED_TIMEOUT)
550 def parse(url, etag, modified):
552 return feedparser.parse(url, etag, modified)
554 proxy = urllib2.ProxyHandler( {"http":PROXY} )
555 return feedparser.parse(url, etag, modified, handlers = [proxy])
558 ### Program Functions ###
561 if len(args) == 2 and contains(args[1], '@') and not contains(args[1], '://'):
562 urls, to = [args[0]], args[1]
564 urls, to = args, None
566 feeds, feedfileObject = load()
567 if (feeds and not isstr(feeds[0]) and to is None) or (not len(feeds) and to is None):
568 print "No email address has been defined. Please run 'r2e email emailaddress' or"
569 print "'r2e add url emailaddress'."
571 for url in urls: feeds.append(Feed(url, to))
572 unlock(feeds, feedfileObject)
575 feeds, feedfileObject = load()
578 # We store the default to address as the first item in the feeds list.
579 # Here we take it out and save it for later.
581 if feeds and isstr(feeds[0]): default_to = feeds[0]; ifeeds = feeds[1:]
584 if num: ifeeds = [feeds[num]]
590 if not f.active: continue
592 if VERBOSE: print >>warn, 'I: Processing [%d] "%s"' % (feednum, f.url)
595 r = timelimit(FEED_TIMEOUT, parse)(f.url, f.etag, f.modified)
597 print >>warn, 'W: feed [%d] "%s" timed out' % (feednum, f.url)
600 # Handle various status conditions, as required
602 if r.status == 301: f.url = r['url']
603 elif r.status == 410:
604 print >>warn, "W: feed gone; deleting", f.url
608 http_status = r.get('status', 200)
609 if VERBOSE > 1: print >>warn, "I: http status", http_status
610 http_headers = r.get('headers', {
611 'content-type': 'application/rss+xml',
612 'content-length':'1'})
613 exc_type = r.get("bozo_exception", Exception()).__class__
614 if http_status != 304 and not r.entries and not r.get('version', ''):
615 if http_status not in [200, 302]:
616 print >>warn, "W: error %d [%d] %s" % (http_status, feednum, f.url)
618 elif contains(http_headers.get('content-type', 'rss'), 'html'):
619 print >>warn, "W: looks like HTML [%d] %s" % (feednum, f.url)
621 elif http_headers.get('content-length', '1') == '0':
622 print >>warn, "W: empty page [%d] %s" % (feednum, f.url)
624 elif hasattr(socket, 'timeout') and exc_type == socket.timeout:
625 print >>warn, "W: timed out on [%d] %s" % (feednum, f.url)
627 elif exc_type == IOError:
628 print >>warn, 'W: "%s" [%d] %s' % (r.bozo_exception, feednum, f.url)
630 elif hasattr(feedparser, 'zlib') and exc_type == feedparser.zlib.error:
631 print >>warn, "W: broken compression [%d] %s" % (feednum, f.url)
633 elif exc_type in socket_errors:
634 exc_reason = r.bozo_exception.args[1]
635 print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
637 elif exc_type == urllib2.URLError:
638 if r.bozo_exception.reason.__class__ in socket_errors:
639 exc_reason = r.bozo_exception.reason.args[1]
641 exc_reason = r.bozo_exception.reason
642 print >>warn, "W: %s [%d] %s" % (exc_reason, feednum, f.url)
644 elif exc_type == AttributeError:
645 print >>warn, "W: %s [%d] %s" % (r.bozo_exception, feednum, f.url)
647 elif exc_type == KeyboardInterrupt:
648 raise r.bozo_exception
651 print >>warn, 'E: error in [%d] "%s" feed (%s)' % (feednum, f.url, r.get("bozo_exception", "can't process"))
654 print >>warn, "=== rss2email encountered a problem with this feed ==="
655 print >>warn, "=== See the rss2email FAQ at http://www.allthingsrss.com/rss2email/ for assistance ==="
656 print >>warn, "=== If this occurs repeatedly, send this to lindsey@allthingsrss.com ==="
657 print >>warn, "E:", r.get("bozo_exception", "can't process"), f.url
659 print >>warn, "rss2email", __version__
660 print >>warn, "feedparser", feedparser.__version__
661 print >>warn, "html2text", h2t.__version__
662 print >>warn, "Python", sys.version
663 print >>warn, "=== END HERE ==="
668 for entry in r.entries:
671 # If TRUST_GUID isn't set, we get back hashes of the content.
672 # Instead of letting these run wild, we put them in context
673 # by associating them with the actual ID (if it exists).
675 frameid = entry.get('id')
676 if not(frameid): frameid = id
677 if type(frameid) is DictType:
678 frameid = frameid.values()[0]
680 # If this item's ID is in our database
681 # then it's already been sent
682 # and we don't need to do anything more.
684 if frameid in f.seen:
685 if f.seen[frameid] == id: continue
687 if not (f.to or default_to):
688 print "No default email address defined. Please run 'r2e email emailaddress'"
689 print "Ignoring feed %s" % f.url
692 if 'title_detail' in entry and entry.title_detail:
693 title = entry.title_detail.value
694 if contains(entry.title_detail.type, 'html'):
695 title = html2text(title)
697 title = getContent(entry)[:70]
699 title = title.replace("\n", " ").strip()
701 datetime = time.gmtime()
704 for datetype in DATE_HEADER_ORDER:
705 kind = datetype+"_parsed"
706 if kind in entry and entry[kind]: datetime = entry[kind]
708 link = entry.get('link', "")
710 from_addr = getEmail(r, entry)
712 name = h2t.unescape(getName(r, entry))
713 fromhdr = formataddr((name, from_addr,))
714 tohdr = (f.to or default_to)
716 datehdr = time.strftime("%a, %d %b %Y %H:%M:%S -0000", datetime)
717 useragenthdr = "rss2email"
719 # Add post tags, if available
722 tags = entry.get('tags')
726 taglist.append(tag['term'])
728 tagline = ",".join(taglist)
730 extraheaders = {'Date': datehdr, 'User-Agent': useragenthdr, 'X-RSS-Feed': f.url, 'X-RSS-ID': id, 'X-RSS-URL': link, 'X-RSS-TAGS' : tagline}
731 if BONUS_HEADER != '':
732 for hdr in BONUS_HEADER.strip().splitlines():
733 pos = hdr.strip().find(':')
735 extraheaders[hdr[:pos]] = hdr[pos+1:].strip()
737 print >>warn, "W: malformed BONUS HEADER", BONUS_HEADER
739 entrycontent = getContent(entry, HTMLOK=HTML_MAIL)
740 contenttype = 'plain'
742 if USE_CSS_STYLING and HTML_MAIL:
745 content += '<head><style><!--' + STYLE_SHEET + '//--></style></head>\n'
746 content += '<body>\n'
747 content += '<div id="entry">\n'
749 content += ' class="header"'
750 content += '><a href="'+link+'">'+subjecthdr+'</a></h1>\n'
751 if ishtml(entrycontent):
752 body = entrycontent[1].strip()
754 body = entrycontent.strip()
756 content += '<div id="body"><table><tr><td>\n' + body + '</td></tr></table></div>\n'
757 content += '\n<p class="footer">URL: <a href="'+link+'">'+link+'</a>'
758 if hasattr(entry,'enclosures'):
759 for enclosure in entry.enclosures:
760 if (hasattr(enclosure, 'url') and enclosure.url != ""):
761 content += ('<br/>Enclosure: <a href="'+enclosure.url+'">'+enclosure.url+"</a>\n")
762 if (hasattr(enclosure, 'src') and enclosure.src != ""):
763 content += ('<br/>Enclosure: <a href="'+enclosure.src+'">'+enclosure.src+'</a><br/><img src="'+enclosure.src+'"\n')
765 for extralink in entry.links:
766 if ('rel' in extralink) and extralink['rel'] == u'via':
767 extraurl = extralink['href']
768 extraurl = extraurl.replace('http://www.google.com/reader/public/atom/', 'http://www.google.com/reader/view/')
770 if ('title' in extralink):
771 viatitle = extralink['title']
772 content += '<br/>Via: <a href="'+extraurl+'">'+viatitle+'</a>\n'
773 content += '</p></div>\n'
774 content += "\n\n</body></html>"
776 if ishtml(entrycontent):
779 content = ("<html><body>\n\n" +
780 '<h1><a href="'+link+'">'+subjecthdr+'</a></h1>\n\n' +
781 entrycontent[1].strip() + # drop type tag (HACK: bad abstraction)
782 '<p>URL: <a href="'+link+'">'+link+'</a></p>' )
784 if hasattr(entry,'enclosures'):
785 for enclosure in entry.enclosures:
786 if enclosure.url != "":
787 content += ('Enclosure: <a href="'+enclosure.url+'">'+enclosure.url+"</a><br/>\n")
789 for extralink in entry.links:
790 if ('rel' in extralink) and extralink['rel'] == u'via':
791 content += 'Via: <a href="'+extralink['href']+'">'+extralink['title']+'</a><br/>\n'
793 content += ("\n</body></html>")
795 content = entrycontent.strip() + "\n\nURL: "+link
796 if hasattr(entry,'enclosures'):
797 for enclosure in entry.enclosures:
798 if enclosure.url != "":
799 content += ('\nEnclosure: ' + enclosure.url + "\n")
801 for extralink in entry.links:
802 if ('rel' in extralink) and extralink['rel'] == u'via':
803 content += '<a href="'+extralink['href']+'">Via: '+extralink['title']+'</a>\n'
805 smtpserver = send(fromhdr, tohdr, subjecthdr, content, contenttype, extraheaders, smtpserver)
809 f.etag, f.modified = r.get('etag', None), r.get('modified', None)
810 except (KeyboardInterrupt, SystemExit):
813 print >>warn, "=== rss2email encountered a problem with this feed ==="
814 print >>warn, "=== See the rss2email FAQ at http://www.allthingsrss.com/rss2email/ for assistance ==="
815 print >>warn, "=== If this occurs repeatedly, send this to lindsey@allthingsrss.com ==="
816 print >>warn, "E: could not parse", f.url
817 traceback.print_exc(file=warn)
818 print >>warn, "rss2email", __version__
819 print >>warn, "feedparser", feedparser.__version__
820 print >>warn, "html2text", h2t.__version__
821 print >>warn, "Python", sys.version
822 print >>warn, "=== END HERE ==="
826 unlock(feeds, feedfileObject)
831 feeds, feedfileObject = load(lock=0)
834 if feeds and isstr(feeds[0]):
835 default_to = feeds[0]; ifeeds = feeds[1:]; i=1
836 print "default email:", default_to
837 else: ifeeds = feeds; i = 0
839 active = ('[ ]', '[*]')[f.active]
840 print `i`+':',active, f.url, '('+(f.to or ('default: '+default_to))+')'
841 if not (f.to or default_to):
842 print " W: Please define a default address with 'r2e email emailaddress'"
846 feeds, feedfileObject = load(lock=0)
849 print '<?xml version="1.0" encoding="UTF-8"?>\n<opml version="1.0">\n<head>\n<title>rss2email OPML export</title>\n</head>\n<body>'
851 url = xml.sax.saxutils.escape(f.url)
852 print '<outline type="rss" text="%s" xmlUrl="%s"/>' % (url, url)
853 print '</body>\n</opml>'
855 def opmlimport(importfile):
856 importfileObject = None
857 print 'Importing feeds from', importfile
858 if not os.path.exists(importfile):
859 print 'OPML import file "%s" does not exist.' % feedfile
861 importfileObject = open(importfile, 'r')
863 print "OPML import file could not be opened: %s" % e
866 dom = xml.dom.minidom.parse(importfileObject)
867 newfeeds = dom.getElementsByTagName('outline')
869 print 'E: Unable to parse OPML file'
872 feeds, feedfileObject = load(lock=1)
875 if f.hasAttribute('xmlUrl'):
876 feedurl = f.getAttribute('xmlUrl')
877 print 'Adding %s' % xml.sax.saxutils.unescape(feedurl)
878 feeds.append(Feed(feedurl, None))
880 unlock(feeds, feedfileObject)
883 feeds, feedfileObject = load()
884 if (n == 0) and (feeds and isstr(feeds[0])):
885 print >>warn, "W: ID has to be equal to or higher than 1"
886 elif n >= len(feeds):
887 print >>warn, "W: no such feed"
889 print >>warn, "W: deleting feed %s" % feeds[n].url
890 feeds = feeds[:n] + feeds[n+1:]
892 print >>warn, "W: feed IDs have changed, list before deleting again"
893 unlock(feeds, feedfileObject)
895 def toggleactive(n, active):
896 feeds, feedfileObject = load()
897 if (n == 0) and (feeds and isstr(feeds[0])):
898 print >>warn, "W: ID has to be equal to or higher than 1"
899 elif n >= len(feeds):
900 print >>warn, "W: no such feed"
902 action = ('Pausing', 'Unpausing')[active]
903 print >>warn, "%s feed %s" % (action, feeds[n].url)
904 feeds[n].active = active
905 unlock(feeds, feedfileObject)
908 feeds, feedfileObject = load()
909 if feeds and isstr(feeds[0]):
913 if VERBOSE: print "Resetting %d already seen items" % len(f.seen)
918 unlock(feeds, feedfileObject)
921 feeds, feedfileObject = load()
922 if feeds and isstr(feeds[0]): feeds[0] = addr
923 else: feeds = [addr] + feeds
924 unlock(feeds, feedfileObject)
926 if __name__ == '__main__':
929 if len(args) < 3: raise InputError, "insufficient args"
930 feedfile, action, args = args[1], args[2], args[3:]
933 if args and args[0] == "--no-send":
934 def send(sender, recipient, subject, body, contenttype, extraheaders=None, smtpserver=None):
935 if VERBOSE: print 'Not sending:', unu(subject)
937 if args and args[-1].isdigit(): run(int(args[-1]))
940 elif action == "email":
942 raise InputError, "Action '%s' requires an argument" % action
946 elif action == "add": add(*args)
948 elif action == "new":
949 if len(args) == 1: d = [args[0]]
951 pickle.dump(d, open(feedfile, 'w'))
953 elif action == "list": list()
955 elif action in ("help", "--help", "-h"): print __doc__
957 elif action == "delete":
959 raise InputError, "Action '%s' requires an argument" % action
960 elif args[0].isdigit():
963 raise InputError, "Action '%s' requires a number as its argument" % action
965 elif action in ("pause", "unpause"):
967 raise InputError, "Action '%s' requires an argument" % action
968 elif args[0].isdigit():
969 active = (action == "unpause")
970 toggleactive(int(args[0]), active)
972 raise InputError, "Action '%s' requires a number as its argument" % action
974 elif action == "reset": reset()
976 elif action == "opmlexport": opmlexport()
978 elif action == "opmlimport":
980 raise InputError, "OPML import '%s' requires a filename argument" % action
984 raise InputError, "Invalid action"
986 except InputError, e: