From aefce1c498b2bb9952d3068d76b1712d1408e3b6 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 8 Dec 2022 21:31:15 +0100 Subject: [PATCH] office: Integrate neomutt and taskwarrior The beginning of what I hope can be a useful integration: send mails to taskwarrior as tasks and open the corresponding mail from tasks in taskwarrior. To make a task out of an e-mail, in neomutt, simply press `t` when the mail is selected or opened. It will create an automatic task in taskwarrior with the description "Reply to [mail] by [sender]" and tag it as mail. If you press `T` instead, you can give the task your own description and tags. In taskwarrior, you can simply `t open ` on a task that came from neomutt to show the message content on the command line (using notmuch). This is still a bit rudimentary and I would like an improved display, but it works for now. --- office/.config/neomutt/maps | 4 ++ office/.config/task/taskopenrc | 3 + office/.local/bin/mutt2task | 100 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100755 office/.local/bin/mutt2task diff --git a/office/.config/neomutt/maps b/office/.config/neomutt/maps index 6d0e3a8..290b75e 100644 --- a/office/.config/neomutt/maps +++ b/office/.config/neomutt/maps @@ -59,3 +59,7 @@ macro index o "export MBSYNC_PRE=true; sync-mail gma # Saner copy/move dialogs macro index C "?" "copy a message to a mailbox" macro index M "?" "move a message to a mailbox" + +# Send mail to taskwarrior +macro index,pager T "mutt2task -c -d -t" "add mail as task to taskwarrior with custom description and tags" +macro index,pager t "mutt2task -c" "add mail as task to taskwarrior" diff --git a/office/.config/task/taskopenrc b/office/.config/task/taskopenrc index c1dd7ac..7c65ed6 100644 --- a/office/.config/task/taskopenrc +++ b/office/.config/task/taskopenrc @@ -13,5 +13,8 @@ notes.command = "$EDITOR ${XDG_DATA_HOME:-~/.local/share}/task/notes/$UUID.md" links.regex = "^https?://" links.command = "xdg-open $FILE" +mail.regex = "^<.*@.*>$" +mail.command = "notmuch show mid:${FILE:1:-1}" + [CLI] diff --git a/office/.local/bin/mutt2task b/office/.local/bin/mutt2task new file mode 100755 index 0000000..71fc583 --- /dev/null +++ b/office/.local/bin/mutt2task @@ -0,0 +1,100 @@ +#!/usr/bin/env python +""" +Creates a new task with corresponding description and tags. + +Description and tags can be supplied via commandline through `-d` and `-t` options, +or interactively by using the options without supplying a string for them. E.g.: + +`mutt2task` => task: Reply to from +`mutt2task -d "hello" -t one two` => task: hello +one +two +`mutt2task -d -t` => asks you interactively for description and tags +""" + +import argparse +import email +import sys +import re + +from tasklib import TaskWarrior, Task + + +def get_args(): + parser = argparse.ArgumentParser( + description="Creates a task with given message and annotates the task with message id" + ) + parser.add_argument( + "-d", + "--description", + nargs="?", + const=True, + default=None, + help="Supply a description for the task. Use the option as a flag to supply description interactively.", + ) + parser.add_argument( + "-t", + "--tags", + nargs="*", + help="Supply any number of tags for the task. Use the option as a flag to supply tags interactively.", + ) + parser.add_argument( + "-c", + "--clean", + action="store_true", + help="Remove subject annoyances like RE FWD on a best-effort basis.", + ) + return parser.parse_args() + + +args = get_args() + +def clean_subject(subject): + to_remove = ["fwd", "re", "fw", "aw", "wg", "sv", "vs", "ref", "tr"] + for abbrev in to_remove: + patt = re.compile(r"^{}(?: |:)\s*".format(abbrev), re.IGNORECASE) + subject = patt.sub("", subject) + return subject + + +def get_task_description(message): + subject = clean_subject(message["subject"]) if args.clean else message["subject"] + description = 'Reply to "{}" from {}'.format( + subject, message["from"].split(" <")[0] + ) + # automatically set description if one provided + if args.description and isinstance(args.description, str): + description = args.description + # otherwise let user enter one if option has been passed + elif args.description: + inp = input("Enter task description or press enter for default: ") + description = inp if inp else description + return description + + +def get_task_tags(): + tags = ["mail"] + if args.tags and len(args.tags) > 0: + tags = sum([tag.split(" ") for tag in args.tags], []) + elif args.tags is not None and len(args.tags) == 0: + inp = input("Enter task tags or press enter for default: ") + tags = inp.split(" ") if inp else tags + return tags + + +def main(): + try: + message = email.message_from_file(sys.stdin) + if not message: + print("Something went wrong, cannot parse mail.") + sys.exit(1) + tw = TaskWarrior("~/.local/share/task", create=False) + sys.stdin = open("/dev/tty") # make user able to add input again + task = Task(tw, description=get_task_description(message), tags=get_task_tags()) + task.save() + task.add_annotation(message['message-id']) + except Exception as e: + print(e) + sys.exit(2) + + +if __name__ == "__main__": + main()