#!/usr/bin/env python # Atomstrom is a small but flexible feed aggregator. # Copyright (C) 2013 Ronald Schaten # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . from models import Base, Feed, Feedinfo, Entry from sqlalchemy import create_engine, desc, func from sqlalchemy.orm import sessionmaker from datetime import datetime from ddate import ddate import sys import codecs import ConfigParser from argparse import ArgumentParser from email.header import Header import smtplib import textwrap def send_mail(sender, receiver, subject, body): print 'sending to %s: %s' % (receiver[0], subject) message = 'From: "%s" <%s>\n' % (Header(sender[0], 'utf-8'), sender[1]) message += 'To: "%s" <%s>\n' % (Header(receiver[0], 'utf-8'), receiver[1]) message += 'Subject: %s\n' % Header(subject, 'utf-8') message += 'Content-Type: text/plain; charset="utf-8"\n\n' message += body.encode('utf-8') server = smtplib.SMTP('localhost') server.sendmail(sender[1], [receiver[1]], message) server.close() def truncate_text(content, length=100, suffix='...'): content = " ".join(content.split()) if len(content) <= length: return content else: return content[:length].rsplit(' ', 1)[0]+suffix def mail_daily_digest(session, sender, receiver, prefix): print 'mailing daily digest...' entries = session.query(Feed, Feedinfo, Entry).\ filter(Feed.id == Feedinfo.feed_id).\ filter(Feed.id == Entry.feed_id).\ filter(Feed.enabled == 1).\ filter(Feed.daily == 1).\ filter(Entry.sent == None).\ order_by(desc(Entry.firstfetched), Feedinfo.title, Entry.title).\ all() body = '' count = 0 for feed, feedinfo, entry in entries: count = count + 1 link = entry.link if entry.resolvedlink: link = entry.resolvedlink text = truncate_text(entry.get_text(enclosures=False), 250) text = textwrap.fill(text, width=78) try: body += '=> %s - %s\n' % (entry.firstfetched.strftime('%Y-%m-%dT%H:%M'), feedinfo.title) body += '> %s\n' % entry.title body += '%s\n' % text body += '%s\n\n' % link except: print 'ERROR processing entry %s' % entry.id; print sys.exc_info() print 'not sending mail' return if count > 0: today = datetime.now() subject = '%s (%s) - %d entries' % (today.strftime('%Y-%m-%d'), today.strftime('%A'), count) body = '%s\n\n%s\n\n%s' % (subject, ddate(), body) if prefix != '': subject = '%s %s' % (prefix, subject) send_mail(sender, receiver, subject, body) for feed, feedinfo, entry in entries: entry.sent = datetime.now() else: print 'no unmailed digest-entries found... not sending mail.' def mail_single_entry(feed, feedinfo, entry, sender, receiver, prefix): subject = '%s' % (entry.title) if prefix != '': subject = '%s %s' % (prefix, subject) link = entry.link if entry.resolvedlink: link = entry.resolvedlink body = entry.get_text() #body = '\n'.join(textwrap.wrap(body, width=78, break_long_words=False, replace_whitespace=False, break_on_hyphens=False)) body += '\n\n%s\n%s\n' % (feedinfo.link, link) sender[0] = feedinfo.title send_mail(sender, receiver, subject, body) entry.sent = datetime.now() def mail_single_entries(session, sender, receiver, prefix): print 'mailing single entries...' count = 0 entries = session.query(Feed, Feedinfo, Entry).\ filter(Feed.id == Feedinfo.feed_id).\ filter(Feed.id == Entry.feed_id).\ filter(Feed.enabled == 1).\ filter(Feed.daily == 0 or Feed.daily == None).\ filter(Entry.sent == None).\ all() for feed, feedinfo, entry in entries: mail_single_entry(feed, feedinfo, entry, sender, receiver, prefix) count = count + 1 if count > 0: print 'sent %d mails' % count else: print 'no unmailed single entries found... not sending mail.' def list_all_feeds(session): allfeeds = session.query(Feed).\ order_by(Feed.id) totalfeeds = 0 totalentries = 0 for feed in allfeeds: print feed totalfeeds += 1 totalentries += len(feed.entries) print 'TOTAL: %d entries in %d feeds.' % (totalentries, totalfeeds) def fetch_all_feeds(session): print 'fetching all feeds...' allfeeds = session.query(Feed).\ filter_by(enabled=1).\ order_by(Feed.id) for feed in allfeeds: feed.fetch(session) print def ask_ok(prompt, retries=4, complaint='Yes or no, please!'): while True: ok = raw_input(prompt) if ok in ('y', 'ye', 'yes'): return True if ok in ('n', 'no'): return False retries = retries - 1 if retries < 0: return False print complaint def feed_ask_delete(session, feed_id): feed = session.query(Feed).\ filter(Feed.id == feed_id).\ first() print feed if ask_ok('> Do you really want to delete feed %d? ' % feed.id): print 'deleting...' session.delete(feed) print '... done.' def feed_ask_reset(session, feed_id): feed = session.query(Feed).\ filter(Feed.id == feed_id).\ first() print feed if ask_ok('> Do you really want to reset feed %d? ' % feed.id): print 'resetting...' feed.reset() print '... done.' def main(): if (sys.stdout.encoding is None): streamWriter = codecs.lookup('utf-8')[-1] sys.stdout = streamWriter(sys.stdout) config = ConfigParser.ConfigParser() config.read('atomstrom.conf') dbconnectstring = '%s://%s:%s@%s/%s?charset=utf8' % ( config.get('database', 'engine'), config.get('database', 'user'), config.get('database', 'password'), config.get('database', 'hostname'), config.get('database', 'database'), ) engine = create_engine(dbconnectstring) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() #session.add(Feed('http://www.heise.de/newsticker/heise-atom.xml', 1, 0, 0, 1, 1)) #session.add(Feed('http://blog.schatenseite.de/feed/', 1, 0, 0, 1, 1)) parser = ArgumentParser(description='Fetch RSS- and Atom-feeds and send mails.') parser.add_argument('-f', '--fetch', action='store_true', help='fetch all feeds') parser.add_argument('-s', '--single', action='store_true', help='send single mails') parser.add_argument('-d', '--daily', action='store_true', help='send daily digest') parser.add_argument('-l', '--list', action='store_true', help='list all configured feeds') parser.add_argument('-e', '--delete', action='store', type=int, metavar='ID', help='delete feed from configuration') parser.add_argument('-r', '--reset', action='store', type=int, metavar='ID', help='reset data for feed ') args = parser.parse_args() if args.fetch: fetch_all_feeds(session) if args.single: sender = [config.get('email', 'sender_name'), config.get('email', 'sender_address')] receiver = [config.get('email', 'receiver_name'), config.get('email', 'receiver_address')] prefix = config.get('email', 'prefix_single') mail_single_entries(session, sender, receiver, prefix) if args.daily: sender = [config.get('email', 'sender_name'), config.get('email', 'sender_address')] receiver = [config.get('email', 'receiver_name'), config.get('email', 'receiver_address')] prefix = config.get('email', 'prefix_digest') mail_daily_digest(session, sender, receiver, prefix) if args.list: list_all_feeds(session) if args.delete: feed_ask_delete(session, args.delete) if args.reset: feed_ask_reset(session, args.reset) if not (args.fetch or args.single or args.daily or args.list or args.delete or args.reset): parser.print_help() session.commit() if __name__ == '__main__': main() # -*- coding: utf-8 -*-