From 13bc68e8bb4a7ad7f7f9302759c6ae4fc124f921 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Thu, 14 Apr 2022 08:01:11 -0700 Subject: Update mutt-ical.py --- mutt-ical.py | 182 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 129 insertions(+), 53 deletions(-) diff --git a/mutt-ical.py b/mutt-ical.py index 69ff4b2..164fb8d 100755 --- a/mutt-ical.py +++ b/mutt-ical.py @@ -1,25 +1,23 @@ -#!/usr/bin/env python -# -*- coding: utf8 -*- +#!/usr/bin/env python3 """ This script is meant as a simple way to reply to ical invitations from mutt. See README for instructions and LICENSE for licensing information. """ -from __future__ import with_statement - __author__="Martin Sander" __license__="MIT" - import vobject import tempfile, time import os, sys import warnings from datetime import datetime -from subprocess import Popen, PIPE +import subprocess from getopt import gnu_getopt as getopt +from email.message import EmailMessage + usage=""" usage: %s [OPTIONS] -e your@email.address filename.ics @@ -29,22 +27,23 @@ OPTIONS: -d decline -t tentatively accept (accept is default, last one wins) + -D display only """ % sys.argv[0] def del_if_present(dic, key): - if dic.has_key(key): + if key in dic: del dic[key] def set_accept_state(attendees, state): for attendee in attendees: - attendee.params['PARTSTAT'][0] = state + attendee.params['PARTSTAT'] = [state] for i in ["RSVP","ROLE","X-NUM-GUESTS","CUTYPE"]: del_if_present(attendee.params,i) return attendees def get_accept_decline(): while True: - sys.stdout.write("Accept Invitation? [Y/n/t]") + sys.stdout.write("\nAccept Invitation? [Y]es/[n]o/[t]entative/[c]ancel\n") ans = sys.stdin.readline() if ans.lower() == 'y\n' or ans == '\n': return 'ACCEPTED' @@ -52,6 +51,9 @@ def get_accept_decline(): return 'DECLINED' elif ans.lower() =='t\n': return 'TENTATIVE' + elif ans.lower() =='c\n': + print("aborted") + sys.exit(1) def get_answer(invitation): # create @@ -61,9 +63,9 @@ def get_answer(invitation): ans.add('vevent') # just copy from invitation - for i in ["uid", "summary", "dtstart", "dtend", "organizer"]: - if invitation.vevent.contents.has_key(i): - ans.vevent.add( invitation.vevent.contents[i][0] ) + for i in ["uid", "summary", "dtstart", "dtend", "organizer", "vtimezone"]: + if i in invitation.vevent.contents: + ans.vevent.add(invitation.vevent.contents[i][0]) # new timestamp ans.vevent.add('dtstamp') @@ -71,24 +73,8 @@ def get_answer(invitation): tzinfo = invitation.vevent.dtstamp.value.tzinfo) return ans -def write_to_tempfile(ical): - tempdir = tempfile.mkdtemp() - icsfile = tempdir+"/event-reply.ics" - with open(icsfile,"w") as f: - f.write(ical.serialize()) - return icsfile, tempdir - -def get_mutt_command(ical, email_address, accept_decline, icsfile): - accept_decline = accept_decline.capitalize() - sender = ical.vevent.contents['organizer'][0].value.split(':')[1].encode() - summary = ical.vevent.contents['summary'][0].value.encode() - command = ["mutt", "-a", icsfile, - "-e", 'set sendmail=\'ical_reply_sendmail_wrapper.sh\'', - "-s", "'%s: %s'" % (accept_decline, summary), "--", sender] - return command - def execute(command, mailtext): - process = Popen(command, stdin=PIPE) + process = subprocess.Popen(command, stdin=subprocess.PIPE) process.stdin.write(mailtext) process.stdin.close() @@ -97,15 +83,95 @@ def execute(command, mailtext): result = process.poll() time.sleep(.1) if result != 0: - print "unable to send reply, subprocess exited with\ - exit code %d\nPress return to continue" % result + print("unable to send reply, subprocess exited with\ + exit code %d\nPress return to continue" % result) sys.stdin.readline() +def openics(invitation_file): + with open(invitation_file) as f: + invitation = vobject.readOne(f, ignoreUnreadable=True) + return invitation + +def format_date(value): + if isinstance(value, datetime): + return value.astimezone(tz=None).strftime("%Y-%m-%d %H:%M %z") + else: + return value.strftime("%Y-%m-%d %H:%M %z") + +def display(ical): + summary = ical.vevent.contents['summary'][0].value + if 'organizer' in ical.vevent.contents: + if hasattr(ical.vevent.organizer,'EMAIL_param'): + sender = ical.vevent.organizer.EMAIL_param + else: + sender = ical.vevent.organizer.value.split(':')[1] #workaround for MS + else: + sender = "NO SENDER" + if 'description' in ical.vevent.contents: + description = ical.vevent.contents['description'][0].value + else: + description = "NO DESCRIPTION" + if 'attendee' in ical.vevent.contents: + attendees = ical.vevent.contents['attendee'] + else: + attendees = "" + if 'location' in ical.vevent.contents: + locations = ical.vevent.contents['location'] + else: + locations = None + sys.stdout.write("From:\t" + sender + "\n") + sys.stdout.write("Title:\t" + summary + "\n") + sys.stdout.write("To:\t") + for attendee in attendees: + if hasattr(attendee,'EMAIL_param') and hasattr(attendee,'CN_param'): + sys.stdout.write(attendee.CN_param + " <" + attendee.EMAIL_param + ">, ") + else: + try: + sys.stdout.write(attendee.CN_param + " <" + attendee.value.split(':')[1] + ">, ") #workaround for MS + except: + sys.stdout.write(attendee.value.split(':')[1] + " <" + attendee.value.split(':')[1] + ">, ") #workaround for 'mailto:' in email + sys.stdout.write("\n") + if hasattr(ical.vevent, 'dtstart'): + print("Start:\t%s" % (format_date(ical.vevent.dtstart.value),)) + if hasattr(ical.vevent, 'dtend'): + print("End:\t%s" % (format_date(ical.vevent.dtend.value),)) + if locations: + sys.stdout.write("Location:\t") + for location in locations: + if location.value: + sys.stdout.write(location.value + ", ") + sys.stdout.write("\n") + sys.stdout.write("\n") + sys.stdout.write(description + "\n") + +def sendmail(): + mutt_setting = subprocess.check_output(["mutt", "-Q", "sendmail"]) + return mutt_setting.strip().decode().split("=")[1].replace('"', '').split() + +def organizer(ical): + if 'organizer' in ical.vevent.contents: + if hasattr(ical.vevent.organizer,'EMAIL_param'): + return ical.vevent.organizer.EMAIL_param + else: + return ical.vevent.organizer.value.split(':')[1] #workaround for MS + else: + raise("no organizer in event") + if __name__=="__main__": email_address = None accept_decline = 'ACCEPTED' - opts, args=getopt(sys.argv[1:],"e:aidt") + opts, args=getopt(sys.argv[1:],"e:aidtD") + + if len(args) < 1: + sys.stderr.write(usage) + sys.exit(1) + + invitation = openics(args[0]) + display(invitation) + for opt,arg in opts: + if opt == '-D': + sys.exit(0) if opt == '-e': email_address = arg if opt == '-i': @@ -117,33 +183,43 @@ if __name__=="__main__": if opt == '-t': accept_decline = 'TENTATIVE' - if len(args) < 1 or not email_address: - sys.stderr.write(usage) - sys.exit(1) - - invitation_file = args[0] - with open(invitation_file) as f: - try: - with warnings.catch_warnings(): #vobject uses deprecated Exception stuff - warnings.simplefilter("ignore") - invitation = vobject.readOne(f, ignoreUnreadable=True) - except AttributeError: - invitation = vobject.readOne(f, ignoreUnreadable=True) - ans = get_answer(invitation) - attendees = invitation.vevent.contents['attendee'] + if 'attendee' in invitation.vevent.contents: + attendees = invitation.vevent.contents['attendee'] + else: + attendees = "" set_accept_state(attendees,accept_decline) - ans.vevent.contents['attendee'] = [i for i in attendees if i.value.endswith(email_address)] - if len(ans.vevent.contents) < 1: + ans.vevent.add('attendee') + ans.vevent.attendee_list.pop() + flag = 1 + for attendee in attendees: + if hasattr(attendee,'EMAIL_param'): + if attendee.EMAIL_param == email_address: + ans.vevent.attendee_list.append(attendee) + flag = 0 + else: + if attendee.value.split(':')[1] == email_address: + ans.vevent.attendee_list.append(attendee) + flag = 0 + if flag: sys.stderr.write("Seems like you have not been invited to this event!\n") sys.exit(1) - icsfile, tempdir = write_to_tempfile(ans) + summary = ans.vevent.contents['summary'][0].value + accept_decline = accept_decline.capitalize() + subject = "'%s: %s'" % (accept_decline, summary) + to = organizer(ans) - mutt_command = get_mutt_command(ans, email_address, accept_decline, icsfile) + message = EmailMessage() + message['From'] = email_address + message['To'] = to + message['Subject'] = subject mailtext = "'%s has %s'" % (email_address, accept_decline.lower()) - execute(mutt_command, mailtext) + message.add_alternative(mailtext, subtype='plain') + message.add_alternative(ans.serialize(), + subtype='calendar', + params={ 'method': 'REPLY' }) + - os.remove(icsfile) - os.rmdir(tempdir) + execute(sendmail() + ['--', to], message.as_bytes()) -- cgit v1.2.3