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 <taskid>` 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.
This commit is contained in:
parent
a89249badd
commit
aefce1c498
3 changed files with 107 additions and 0 deletions
|
@ -59,3 +59,7 @@ macro index o "<sync-mailbox><shell-escape>export MBSYNC_PRE=true; sync-mail gma
|
|||
# Saner copy/move dialogs
|
||||
macro index C "<copy-message>?<toggle-mailboxes>" "copy a message to a mailbox"
|
||||
macro index M "<save-message>?<toggle-mailboxes>" "move a message to a mailbox"
|
||||
|
||||
# Send mail to taskwarrior
|
||||
macro index,pager T "<pipe-message>mutt2task -c -d -t<enter>" "add mail as task to taskwarrior with custom description and tags"
|
||||
macro index,pager t "<pipe-message>mutt2task -c<enter>" "add mail as task to taskwarrior"
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
100
office/.local/bin/mutt2task
Executable file
100
office/.local/bin/mutt2task
Executable file
|
@ -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 <subject> from <sender>
|
||||
`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()
|
Loading…
Reference in a new issue