2 """rss2email: get RSS feeds emailed to you
3 http://www.aaronsw.com/2002/rss2email
5 Usage: python rss2email.py feedfile action [options]
6 feedfile: name of the file to store feed info in
8 new [youremail] (create new feedfile)
9 email [yournewemail] (update default email)
11 add feedurl [youremail]
16 __author__ = "Aaron Swartz (me@aaronsw.com)"
17 __copyright__ = "(C) 2004 Aaron Swartz. GNU GPL 2."
18 ___contributors__ = ["Dean Jackson (dino@grorg.org)",
19 "Brian Lalor (blalor@ithacabands.org)",
20 "Joey Hess", 'Matej Cepl']
22 ### Vaguely Customizable Options ###
24 # The email address messages are from by default:
25 DEFAULT_FROM = "bozo@dev.null"
27 # 1: Only use the DEFAULT_FROM address.
28 # 0: Use the email address specified by the feed, when possible.
31 # 1: Receive one email per post
32 # 0: Receive an email every time a post changes
35 # 1: Generate Date header based on item's date, when possible
36 # 0: Generate Date header based on time sent
39 # 1: Treat the contents of <description> as HTML
40 # 0: Send the contents of <description> as is, without conversion
41 TREAT_DESCRIPTION_AS_HTML = 1
43 # 1: Apply Q-P conversion (required for some MUAs)
44 # 0: Send message in 8-bits
45 # http://cr.yp.to/smtp/8bitmime.html
48 # 1: Name feeds as they're being processed.
52 def send(fr, to, message):
53 i, o = os.popen2(["/usr/sbin/sendmail", to])
58 # def send(fr, to, message):
60 # s = smtplib.SMTP("vorpal.notabug.com:26")
61 # s.sendmail(fr, [to], message)
63 ### End of Options ###
65 # Read options from config file if present.
73 from html2text import html2text, expandEntities
75 import cPickle as pickle, fcntl, md5, time, os, traceback
76 if QP_REQUIRED: import mimify; from StringIO import StringIO as SIO
77 def isstr(f): return isinstance(f, type('')) or isinstance(f, type(u''))
79 def e(obj, val, ee=1):
81 if ee: x = expandEntities(x)
82 if type(x) is unicode: x = x.encode('utf-8')
85 def quoteEmailName(s):
86 return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
88 def getContent(item, url):
89 if item.has_key('content') and item['content']:
90 for c in item['content']:
91 if c['type'] == 'text/plain': return e(c, 'value')
93 for c in item['content']:
94 if c['type'].find('html') != -1:
95 return html2text(e(c, 'value', ee=0), c['base'])
97 return e(item['content'][0], 'value')
99 if item.has_key('description'):
100 if TREAT_DESCRIPTION_AS_HTML:
101 return html2text(e(item, 'description', ee=0), url)
103 return e(item, 'description')
105 if item.has_key('summary'): return e(item, 'summary')
108 def getID(item, content):
110 if item.has_key('id') and item['id']: return e(item, 'id')
112 if content: return md5.new(content).hexdigest()
113 if item.has_key('link'): return e(item, 'link')
114 if item.has_key('title'): return md5.new(e(item, 'title')).hexdigest()
117 def __init__(self, url, to):
118 self.url, self.etag, self.modified, self.seen = url, None, None, {}
122 ff2 = open(feedfile, 'r')
123 feeds = pickle.load(ff2)
125 fcntl.flock(ff2, fcntl.LOCK_EX)
126 #HACK: to deal with lock caching
127 ff2 = open(feedfile, 'r')
128 feeds = pickle.load(ff2)
129 fcntl.flock(ff2, fcntl.LOCK_EX)
133 def unlock(feeds, ff2):
134 pickle.dump(feeds, open(feedfile+'.tmp', 'w'))
135 os.rename(feedfile+'.tmp', feedfile)
136 fcntl.flock(ff2, fcntl.LOCK_UN)
138 def add(url, to=None):
140 if not isstr(feeds[0]) and to is None:
141 raise 'NoEmail', "Run `email newaddr` or `add url addr`."
142 feeds.append(Feed(url, to))
148 if isstr(feeds[0]): default_to = feeds[0]; ifeeds = feeds[1:]
153 if VERBOSE: print "Processing", f.url
154 result = feedparser.parse(f.url, f.etag, f.modified)
156 if result.has_key('status') and result['status'] == 301: f.url = result['url']
158 if result.has_key('encoding'): enc = result['encoding']
161 c, ert = result['channel'], 'errorreportsto'
164 if c.has_key('title'): headers += quoteEmailName(e(c, 'title')) + ' '
165 if FORCE_FROM and c.has_key(ert) and c[ert].startswith('mailto:'):
170 headers += '<'+fr+'>'
172 headers += "\nTo: " + (f.to or default_to) # set a default email!
174 headers += '\nContent-Type: text/plain; charset="' + enc + '"'
176 if not result['items'] and ((not result.has_key('status') or (result.has_key('status') and result['status'] != 304))):
177 print "W: no items; invalid feed? (" + f.url + ")"
180 for i in result['items']:
181 content = getContent(i, f.url)
182 id = getID(i, content)
184 if i.has_key('link') and i['link']: link = e(i, 'link')
187 if i.has_key('id') and i['id']: frameid = e(i, 'id')
190 if f.seen.has_key(frameid) and f.seen[frameid] == id:
193 if i.has_key('title'): title = e(i, 'title')
194 else: title = content[:70].replace("\n", " ")
196 if DATE_HEADER and i.has_key('date_parsed'):
197 datetime = i['date_parsed']
199 datetime = time.gmtime()
202 + "\nSubject: " + title
203 + "\nDate: " + time.strftime("%a, %d %b %Y %H:%M:%S -0000", datetime)
204 + "\nUser-Agent: rss2email"
207 message += "\n" + content.strip() + "\n"
209 if link: message += "\nURL: " + link + "\n"
213 ins, outs = SIO(message), SIO()
214 mimify.mimify(ins, outs); outs.seek(0)
215 message = outs.read()
217 send(fr, (f.to or default_to), message)
221 f.etag, f.modified = result.get('etag', None), result.get('modified', None)
223 print "E: could not parse", f.url
224 traceback.print_exc()
231 feeds, ff2 = load(lock=0)
234 default_to = feeds[0]; ifeeds = feeds[1:]; i=1
235 print "default email:", default_to
236 else: ifeeds = feeds; i = 0
238 print `i`+':', f.url, '('+(f.to or ('default: '+default_to))+')'
243 feeds = feeds[:n] + feeds[n+1:]
248 if isstr(feeds[0]): feeds[0] = addr
249 else: feeds = [addr] + feeds
252 if __name__ == '__main__':
253 ie, args = "InputError", sys.argv
255 if len(args) < 3: raise ie, "insufficient args"
256 feedfile, action, args = args[1], args[2], args[3:]
259 if args and args[0] == "--no-send":
261 if VERBOSE: print 'Not sending', (
262 [x for x in z.splitlines() if x.startswith("Subject:")][0])
265 elif action == "email":
267 print "W: Feed IDs may have changed. Run `list` before `delete`."
269 elif action == "add": add(*args)
271 elif action == "new":
272 if len(args) == 1: d = [args[0]]
274 pickle.dump(d, open(feedfile, 'w'))
276 elif action == "list": list()
278 elif action == "delete": delete(int(args[0]))
281 raise ie, "invalid action"