Update 2022-12-08 13:17 OpenBSD/amd64
This commit is contained in:
223
.mutt/scripts/mutt-ical.py
Executable file
223
.mutt/scripts/mutt-ical.py
Executable file
@@ -0,0 +1,223 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user