#!/usr/bin/env python # todo-email-scraper.py - Watch an email address for TODOs and add them to an org file # # Copyright (c) 2014 Free Software Foundation, Inc. # # Authors: # Anthony Lander # # Version: 1.0 # Keywords: org, todo, email # # This file is not part of GNU Emacs. # # 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, 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 GNU Emacs. If not, see . # # Commentary: # # - Email subject is the TODO line, and body is the TODO body. # - Add a property line :TODO-TARGET: true to the parent heading where TODOs should be dropped # - Configure email address, diary file, password below. # - You can schedule this to run every 30 minutes in a cron job. # - The scraper scrapes all emails from a designated address (so only you can # - send yourself todos). as such, it is best to set up a todo email address to # - receive only todo emails. import sys from os import rename from datetime import datetime from string import replace import imaplib import StringIO from email.parser import Parser # ### Configuration diary_file = "/path/to/org_file.org" # Replace with your org file server = 'imap.gmail.com' # Replace with your imap mail server username = 'tasklist@gmail.com' # Replace with the email address that receives todos password = 'passowrd' # Replace with the password for the above email address authorized_sender = 'you@gmail.com' # Replace with the email address from which you send todos def is_diary_file_available(): try: file = open(diary_file, "r") except IOError: return False file.close() return True def get_todos(): imap = imaplib.IMAP4_SSL(server) imap.login(username, password) imap.select() search_criteria = '(UNSEEN FROM "' + authorized_sender + '")' typ, data = imap.search(None, search_criteria) todos = [] for num in data[0].split(): todo_subject = "" todo_body = [] typ, data = imap.fetch(num, '(RFC822)') file = StringIO.StringIO(data[0][1]) message = Parser().parse(file) todo_subject = message['subject'] # print message['Subject'] + "\n" body = "" for part in message.walk(): t = part.get_content_type() if t and t.lower() == "text/plain": # Found the first text/plain part body = part.get_payload(decode=True) break # print ">", body, "<" long_line = "" for line in [line.strip() for line in body.splitlines() if len(line) > 0]: if line[-1:] == "=": long_line += line[:-1] else: if len(long_line) > 0: # There is a long line waiting to be written # print "long line: ", long_line todo_body.append(long_line) long_line = "" else: # print "line: ", line todo_body.append(line) long_line = "" if len(long_line) > 0: # print "long line (final): ", long_line todo_body.append(long_line) todos.append({'subject' : todo_subject, 'body' : todo_body}) imap.close() imap.logout() return todos def new_diary_lines_with_todos(todos): try: file = open(diary_file, "rt+") found = False done = False stars = "" timestamp = datetime.now().strftime("[%Y-%m-%d %a %H:%M]") log_lines = [":LOGBOOK:", '- State "TODO" from "" ' + timestamp, ":END:"] lines = [] while not done: line = file.readline() if len(line) == 0: done = True else: lines.append(line) if not found: if line[0] == "*": stars = line.split()[0] if line.strip() == ":TODO-TARGET: true": found = True line = "" while not line.strip() == ":END:": line = file.readline() lines.append(line) for todo in todos: line = "%s%s%s%s" % (stars, "** TODO ", todo['subject'], "\n") lines.append(line) spaces = replace(stars + "** ", "*", " ") for line in log_lines: lines.append(spaces + line + "\n") for line in todo['body']: lines.append(spaces + line + "\n") file.close() except: print "Unexpected error:", sys.exc_info()[0] file.close() raise return lines def write_todo_file(filename, todos): try: file = open(filename, "w") for todo in todos: file.write(todo) file.close() return True except: print "Unexpected error:", sys.exc_info()[0] file.close() raise return False def scrape_todos(): if not is_diary_file_available(): print "No diary file available - aborting." sys.exit(-1) todos = get_todos() if len(todos) > 0: new_lines = new_diary_lines_with_todos(todos) try: now = datetime.now().strftime(".%Y-%m-%d-%H-%M") diary_backup = diary_file + now + ".orig" # print "renaming", diary_file, " to ", diary_backup rename(diary_file, diary_backup) except: print "Unexpected error while renaming:", sys.exc_info()[0] raise write_todo_file(diary_file, new_lines) print "Added ", len(todos), " todos at ", now if __name__ == '__main__': scrape_todos() sys.exit(0)