224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
|
#!/usr/bin/env python
|
||
|
# -*- coding: utf8 -*-
|
||
|
|
||
|
"""
|
||
|
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"
|
||
|
|
||
|
|
||
|
from tzlocal import get_localzone
|
||
|
import pytz
|
||
|
import vobject
|
||
|
import tempfile, time
|
||
|
import os, sys
|
||
|
import warnings
|
||
|
from datetime import date, datetime
|
||
|
from subprocess import Popen, PIPE
|
||
|
from getopt import gnu_getopt as getopt
|
||
|
|
||
|
timezone = get_localzone()
|
||
|
mutt="mutt"
|
||
|
|
||
|
usage="""
|
||
|
usage:
|
||
|
%s [OPTIONS] -e your@email.address filename.ics
|
||
|
OPTIONS:
|
||
|
-i interactive
|
||
|
-a accept
|
||
|
-d decline
|
||
|
-t tentatively accept
|
||
|
-c mutt_command
|
||
|
(accept is default, last one wins)
|
||
|
""" % sys.argv[0]
|
||
|
|
||
|
def del_if_present(dic, key):
|
||
|
if dic.has_key(key):
|
||
|
del dic[key]
|
||
|
|
||
|
def set_accept_state(attendees, state):
|
||
|
for attendee in attendees:
|
||
|
attendee.params['PARTSTAT'] = [unicode(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("\nAccept Invitation? [y/n/t/q]")
|
||
|
ans = sys.stdin.readline()
|
||
|
if ans.lower() == 'y\n':
|
||
|
return 'ACCEPTED'
|
||
|
elif ans.lower() == 'n\n':
|
||
|
return 'DECLINED'
|
||
|
elif ans.lower() == 't\n':
|
||
|
return 'TENTATIVE'
|
||
|
elif ans.lower() == 'q\n':
|
||
|
return ''
|
||
|
|
||
|
def get_answer(invitation):
|
||
|
# create
|
||
|
ans = vobject.newFromBehavior('vcalendar')
|
||
|
ans.add('method')
|
||
|
ans.method.value = "REPLY"
|
||
|
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] )
|
||
|
|
||
|
# new timestamp
|
||
|
ans.vevent.add('dtstamp')
|
||
|
ans.vevent.dtstamp.value = datetime.utcnow().replace(
|
||
|
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()
|
||
|
if ical.vevent.contents.has_key('organizer'):
|
||
|
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"
|
||
|
summary = ical.vevent.contents['summary'][0].value.encode()
|
||
|
command = [mutt, "-e", "my_hdr From: %s" % email_address, "-a", icsfile,
|
||
|
"-s", "%s: %s" % (accept_decline, summary), "--", sender]
|
||
|
#Uncomment the below line, and move it above the -s line to enable the wrapper
|
||
|
#"-e", 'set sendmail=\'ical_reply_sendmail_wrapper.sh\'',
|
||
|
return command
|
||
|
|
||
|
def execute(command, mailtext):
|
||
|
process = Popen(command, stdin=PIPE)
|
||
|
process.stdin.write(mailtext)
|
||
|
process.stdin.close()
|
||
|
|
||
|
result = None
|
||
|
while result is None:
|
||
|
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
|
||
|
sys.stdin.readline()
|
||
|
|
||
|
def openics(invitation_file):
|
||
|
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)
|
||
|
return invitation
|
||
|
|
||
|
def display(ical):
|
||
|
summary = ical.vevent.contents['summary'][0].value.encode()
|
||
|
if ical.vevent.contents.has_key('organizer'):
|
||
|
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 ical.vevent.contents.has_key('description'):
|
||
|
description = ical.vevent.contents['description'][0].value
|
||
|
else:
|
||
|
description = "NO DESCRIPTION"
|
||
|
if ical.vevent.contents.has_key('attendee'):
|
||
|
attendees = ical.vevent.contents['attendee']
|
||
|
else:
|
||
|
attendees = ""
|
||
|
|
||
|
sys.stdout.write("Start:\t" + ical.vevent.dtstart.value.astimezone(timezone).strftime('%Y-%m-%d %I:%M %p %Z') + "\n")
|
||
|
sys.stdout.write("End:\t" + ical.vevent.dtend.value.astimezone(timezone).strftime('%Y-%m-%d %I:%M %p %Z') + "\n")
|
||
|
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'):
|
||
|
sys.stdout.write(attendee.CN_param + " <" + attendee.EMAIL_param + ">, ")
|
||
|
else:
|
||
|
sys.stdout.write(attendee.CN_param + " <" + attendee.value.split(':')[1] + ">, ") #workaround for MS
|
||
|
sys.stdout.write("\n\n")
|
||
|
sys.stdout.write(description + "\n")
|
||
|
|
||
|
if __name__=="__main__":
|
||
|
email_address = None
|
||
|
email_addresses = []
|
||
|
accept_decline = ''
|
||
|
opts, args=getopt(sys.argv[1:],"e:aidtc:")
|
||
|
|
||
|
if len(args) < 1:
|
||
|
sys.stderr.write(usage)
|
||
|
sys.exit(1)
|
||
|
|
||
|
invitation = openics(args[0])
|
||
|
#print(invitation)
|
||
|
display(invitation)
|
||
|
|
||
|
for opt,arg in opts:
|
||
|
if opt == '-e':
|
||
|
email_addresses = arg.split(',')
|
||
|
if opt == '-i':
|
||
|
accept_decline = get_accept_decline()
|
||
|
if opt == '-a':
|
||
|
accept_decline = 'ACCEPTED'
|
||
|
if opt == '-d':
|
||
|
accept_decline = 'DECLINED'
|
||
|
if opt == '-t':
|
||
|
accept_decline = 'TENTATIVE'
|
||
|
if opt == '-c':
|
||
|
mutt = arg
|
||
|
|
||
|
if accept_decline == '':
|
||
|
sys.exit(0)
|
||
|
|
||
|
ans = get_answer(invitation)
|
||
|
|
||
|
if invitation.vevent.contents.has_key('attendee'):
|
||
|
attendees = invitation.vevent.contents['attendee']
|
||
|
else:
|
||
|
attendees = ""
|
||
|
set_accept_state(attendees,accept_decline)
|
||
|
ans.vevent.add('attendee')
|
||
|
ans.vevent.attendee_list.pop()
|
||
|
flag = 1
|
||
|
for attendee in attendees:
|
||
|
if hasattr(attendee,'EMAIL_param'):
|
||
|
if attendee.EMAIL_param in email_addresses:
|
||
|
ans.vevent.attendee_list.append(attendee)
|
||
|
email_address = attendee.EMAIL_param
|
||
|
flag = 0
|
||
|
else:
|
||
|
if attendee.value.split(':')[1] in email_addresses:
|
||
|
ans.vevent.attendee_list.append(attendee)
|
||
|
email_address = attendee.value.split(':')[1]
|
||
|
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)
|
||
|
|
||
|
mutt_command = get_mutt_command(ans, email_address, accept_decline, icsfile)
|
||
|
mailtext = "From: %s\n\n%s has %s" % (email_address, email_address, accept_decline.lower())
|
||
|
execute(mutt_command, mailtext)
|
||
|
|
||
|
os.remove(icsfile)
|
||
|
os.rmdir(tempdir)
|