This article was originally published by Python Magazine in October of 2007.
Originally published in Python Magazine Volume 1 Issue 10 , October, 2007
How can you access group calendar information if your Exchange-like mail and calendaring server does not provide iCalendar feeds, and you do not, or cannot, use Outlook? Use Python to extract the calendar data and generate your own feed, of course! This article discusses a surprisingly simple program to perform what seems like a complex series of operations: scanning IMAP folders, extracting iCalendar attachments, and merging the contained events together into a single calendar.
I recently needed to access shared schedule information stored on an Exchange-like mail and calendaring server. Luckily, I was able to combine an existing third party open source library with the tools in the Python standard library to create a command line program to convert the calendar data into a format I could use with my desktop client directly. The final product is called mailbox2ics. It ended up being far shorter than I had anticipated when I started thinking about how to accomplish my goal. The entire program is just under 140 lines long, including command line switch handling, some error processing, and debug statements. The output file produced can be consumed by any scheduling client that supports the iCalendar standard.
Using Exchange, or a compatible replacement, for email and scheduling makes sense for many businesses and organizations. The client program, Microsoft Outlook, is usually familiar to non-technical staff members, and therefore new hires can hit the ground running instead of being stymied trying to figure out how to accomplish their basic, everyday communication tasks. However, my laptop runs Mac OS X and I do not have Outlook. Purchasing a copy of Outlook at my own expense, in addition to inflicting further software bloat on my already crowded computer, seemed like an unnecessarily burdensome hassle just to be able to access schedule information.
Changing the server software was not an option. A majority of the users already had Outlook and were accustomed to using it for their scheduling, and I did not want to have to support a different server platform. That left me with one option: invent a way to pull the data out of the existing server, so I could convert it to a format that I could use with my usual tools: Apple’s iCal and Mail.
With iCal (and many other standards-compliant calendar tools) it is possible to subscribe to calendar data feeds. Unfortunately, the server we were using did not have the ability to export the schedule data in a standard format using a single file or URL. However, the server did provide access to the calendar data via IMAP using shared public folders. I decided to use Python to write a program to extract the data from the server and convert it into a usable feed. The feed would be passed to iCal, which would merge the group schedule with the rest of my calendar information so I could see the group events alongside my other meetings, deadlines, and reminders about when the recycling is picked up on our street.
The calendar data was only accessible to me as attachments on email messages accessed via an IMAP server. The messages were grouped into several folders, with each folder representing a separate public calendar used for a different purpose (meeting room schedules, event planning, holiday and vacation schedules, etc.). I had read-only access to all of the email messages in the public calendar folders. Each email message typically had one attachment describing a single event. To produce the merged calendar, I needed to scan several folders, read each message in the folder, find and parse the calendar data in the attachments, and identify the calendar events. Once I identified the events to include in the output, I needed to add them to an output file in a format iCal understands.
Python’s standard library includes the imaplib module for working with IMAP servers. The IMAP4 and IMAP4_SSL classes provide a high level interface to all of the features I needed: connecting to the server securely, accessing mailboxes, finding messages, and downloading them. To experiment with retrieving data from the IMAP server, I started by establishing a secure connection to the server on the standard port for IMAP-over-SSL, and logging in using my regular account. This would not be a desirable way to run the final program on a regular basis, but it works fine for development and testing.
mail_server = imaplib.IMAP4_SSL(hostname) mail_server.login(username, password)
It is also possible to use IMAP over a non-standard port. In that case, the caller can pass port as an additional option to imaplib.IMAP4_SSL(). To work with an IMAP server without SSL encryption, you can use the IMAP4 class, but using SSL is definitely preferred.
mail_server = imaplib.IMAP4_SSL(hostname, port) mail_server.login(username, password)
The connection to the IMAP server is “stateful”. The client remembers which methods have been called on it, and changes its internal state to reflect those calls. The internal state is used to detect logical errors in the sequence of method calls without the round-trip to the server.
On an IMAP server, messages are organized into “mailboxes”. Each mailbox has a name and, since mailboxes might be nested, the full name of the mailbox is the path to that mailbox. Mailbox paths work just like paths to directories or folders in a filesystem. The paths are single strings, with levels usually separated by a forward slash (/) or period (.). The actual separator value used depends on the configuration of your IMAP server; one of my servers uses a slash, while the other uses a period. If you do not already know how your server is set up, you will need to experiment to determine the correct values for folder names.
Once I had my client connected to the server, the next step was to call select() to set the mailbox context to be used when searching for and downloading messages.
mail_server.select('Public Folders/EventCalendar') # or mail_server.select('Public Folders.EventCalendar')
After a mailbox is selected, it is possible to retrieve messages from the mailbox using search(). The IMAP method search() supports filtering to identify only the messages you need. You can search for messages based on the content of the message headers, with the rules evaluated in the server instead of your client, thus reducing the amount of information the server has to transmit to the client. Refer to RFC 3501 (“Internet Message Access Protocol”) for details about the types of queries which can be performed and the syntax for passing the query arguments.
In order to implement mailbox2ics, I needed to look at all of the messages in every mailbox the user named on the command line, so I simply used the filter "ALL" with each mailbox. The return value from search() includes a response code and a string with the message numbers separated by spaces. A separate call is required to retrieve more details about an individual message, such as the headers or body.
(typ, [message_ids]) = mail_server.search(None, 'ALL') message_ids = message_ids.split()
Individual messages are retrieved via fetch(). If only part of the message is desired (size, envelope, body), that part can be fetched to limit bandwidth. I could not predict which subset of the message body might include the attachments I wanted, so it was simplest for me to download the entire message. Calling fetch("(RFC822)") returns a string containing the MIME-encoded version of the message with all headers intact.
typ, message_parts = mail_server.fetch( message_ids, '(RFC822)') message_body = message_parts
Once the message body had been downloaded, the next step was to parse it to find the attachments with calendar data. Beginning with version 2.2.3, the Python standard library has included the email package for working with standards-compliant email messages. There is a straightforward factory for converting message text to Message objects. To parse the text representation of an email and create a Message instance from it, use email.message_from_string().
msg = email.message_from_string(message_body)
Message objects are almost always made up of multiple parts. The parts of the message are organized in a tree structure, with message attachments supporting nested attachments. Subparts or attachments can even include entire email messages, such as when you forward a message which already contains an attachment to someone else. To iterate over all of the parts of the Message tree recursively, use the walk() method.
for part in msg.walk(): print part.get_content_type()
Having access to the email package saved an enormous amount of time on this project. Parsing multi-part email messages reliably is tricky, even with (or perhaps because of) the many standards involved. With the email package, in just a few lines of Python, you can parse and traverse all of the parts of even the most complex standard-compliant multi-part email message, giving you access to the type and content of each part.
The “Internet Calendaring and Scheduling Core Object Specification”, or iCalendar, is defined in RFC 2445. iCalendar is a data format for sharing scheduling and other date-oriented information. One typical way to receive an iCalendar event notification, such as an invitation to a meeting, is via an email attachment. Most standard calendaring tools, such as iCal and Outlook, generate these email messages when you initially “invite” another participant to a meeting, or update an existing meeting description. The iCalendar standard says the file should have filename extension ICS and mime-type text/calendar. The input data for mailbox2ics came from email attachments of this type.
The iCalendar format is text-based. A simple example of an ICS file with a single event is provided in Listing 1. Calendar events have properties to indicate who was invited to an event, who originated it, where and when it will be held, and all of the other expected bits of information important for a scheduled event. Each property of the event is encoded on its own line, with long values wrapped onto multiple lines in a well-defined way to allow the original content to be reconstructed by a client receiving the iCalendar representation of the data. Some properties also can be repeated, to handle cases such as meetings with multiple invitees.
BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//Big Calendar Corp//Server Version X.Y.Z//EN VERSION:2.0 METHOD:PUBLISH BEGIN:VEVENT UID:20379258.1177945519186.JavaMail.root(a)imap.example.com LAST-MODIFIED:20070519T000650Z DTSTAMP:20070519T000650Z DTSTART;VALUE=DATE:20070508 DTEND;VALUE=DATE:20070509 PRIORITY:5 TRANSP:OPAQUE SEQUENCE:0 SUMMARY:Day off LOCATION: CLASS:PUBLIC END:VEVENT END:VCALENDAR
In addition to having a variety of single or multi-value properties, calendar elements can be nested, much like email messages with attachments. An ICS file is made up of a VCALENDAR component, which usually includes one or more VEVENT components. A VCALENDAR might also include VTODO components (for tasks on a to-do list). A VEVENT may contain a VALARM, which specifies the time and means by which the user should be reminded of the event. The complete description of the iCalendar format, including valid component types and property names, and the types of values which are legal for each property, is available in the RFC.
This sounds complex, but luckily, I did not have to worry about parsing the ICS data at all. Instead of doing the work myself, I took advantage of an open source Python library for working with iCalendar data released by Max M. (firstname.lastname@example.org). His iCalendar library (available from codespeak.net) makes parsing ICS data sources very simple. The API for the library was designed based on the email package discussed previously, so working with Calendar instances and email.Message instances is similar. Use the class method Calendar.from_string() to parse the text representation of the calendar data to create a Calendar instance populated with all of the properties and subcomponents described in the input data.
from icalendar import Calendar, Event cal_data = Calendar.from_string(open('sample.ics', 'rb').read())
Once you have instantiated the Calendar object, there are two different ways to iterate through its components: via the walk() method or subcomponents attribute. Using walk() will traverse the entire tree and let you process each component in the tree individually. Accessing the subcomponents list directly lets you work with a larger portion of the calendar data tree at one time. Properties of an individual component, such as the summary or start date, are accessed via the __getitem__() API, just as with a standard Python dictionary. The property names are not case sensitive.
For example, to print the “SUMMARY” field values from all top level events in a calendar, you would first iterate over the subcomponents, then check the name attribute to determine the component type. If the type is VEVENT, then the summary can be accessed and printed.
for event in cal_data.subcomponents: if event.name == 'VEVENT': print 'EVENT:', event['SUMMARY']
While most of the ICS attachments in my input data would be made up of one VCALENDAR component with one VEVENT subcomponent, I did not want to require this limitation. The calendars are writable by anyone in the organization, so while it was unlikely that anyone would have added a VTODO or VJOURNAL to public data, I could not count on it. Checking for VEVENT as I scanned each component let me ignore components with types that I did not want to include in the output.
Writing ICS data to a file is as simple as reading it, and only takes a few lines of code. The Calendar class handles the difficult tasks of encoding and formatting the data as needed to produce a fully formatted ICS representation, so I only needed to write the formatted text to a file.
ics_output = open('output.ics', 'wb') try: ics_output.write(str(cal_data)) finally: ics_output.close()
Finding Max M’s iCalendar library saved me a lot of time and effort, and demonstrates clearly the value of Python and open source in general. The API is concise and, since it is patterned off of another library I was already using, the idioms were familiar. I had not embarked on this project eager to write parsers for the input data, so I was glad to have libraries available to do that part of the work for me.
At this point, I had enough pieces to build a program to do what I needed. I could read the email messages from the server via IMAP, parse each message, and then search through its attachments to find the ICS attachments. Once I had the attachments, I could parse them and produce another ICS file to be imported into my calendar client. All that remained was to tie the pieces together and give it a user interface. The source for the resulting program, mailbox2ics.py, is provided in Listing 2.
#!/usr/bin/env python # mailbox2ics.py """Convert the contents of an imap mailbox to an ICS file. This program scans an IMAP mailbox, reads in any messages with ICS files attached, and merges them into a single ICS file as output. """ # Import system modules import imaplib import email import getpass import optparse import sys # Import Local modules from icalendar import Calendar, Event # Module def main(): # Set up our options option_parser = optparse.OptionParser( usage='usage: %prog [options] hostname username mailbox [mailbox...]' ) option_parser.add_option('-p', '--password', dest='password', default='', help='Password for username', ) option_parser.add_option('--port', dest='port', help='Port for IMAP server', type="int", ) option_parser.add_option('-v', '--verbose', dest="verbose", action="store_true", default=True, help='Show progress', ) option_parser.add_option('-q', '--quiet', dest="verbose", action="store_false", help='Do not show progress', ) option_parser.add_option('-o', '--output', dest="output", help="Output file", default=None, ) (options, args) = option_parser.parse_args() if len(args) < 3: option_parser.print_help() print >>sys.stderr, '\nERROR: Please specify a username, hostname, and mailbox.' return 1 hostname = args username = args mailboxes = args[2:] # Make sure we have the credentials to login to the IMAP server. password = options.password or getpass.getpass(stream=sys.stderr) # Initialize a calendar to hold the merged data merged_calendar = Calendar() merged_calendar.add('prodid', '-//mailbox2ics//doughellmann.com//') merged_calendar.add('calscale', 'GREGORIAN') if options.verbose: print >>sys.stderr, 'Logging in to "%s" as %s' % (hostname, username) # Connect to the mail server if options.port is not None: mail_server = imaplib.IMAP4_SSL(hostname, options.port) else: mail_server = imaplib.IMAP4_SSL(hostname) (typ, [login_response]) = mail_server.login(username, password) try: # Process the mailboxes for mailbox in mailboxes: if options.verbose: print >>sys.stderr, 'Scanning %s ...' % mailbox (typ, [num_messages]) = mail_server.select(mailbox) if typ == 'NO': raise RuntimeError('Could not find mailbox %s: %s' % (mailbox, num_messages)) num_messages = int(num_messages) if not num_messages: if options.verbose: print >>sys.stderr, ' empty' continue # Find all messages (typ, [message_ids]) = mail_server.search(None, 'ALL') for num in message_ids.split(): # Get a Message object typ, message_parts = mail_server.fetch(num, '(RFC822)') msg = email.message_from_string(message_parts) # Look for calendar attachments for part in msg.walk(): if part.get_content_type() == 'text/calendar': # Parse the calendar attachment ics_text = part.get_payload(decode=1) importing = Calendar.from_string(ics_text) # Add events from the calendar to our merge calendar for event in importing.subcomponents: if event.name != 'VEVENT': continue if options.verbose: print >>sys.stderr, 'Found: %s' % event['SUMMARY'] merged_calendar.add_component(event) finally: # Disconnect from the IMAP server if mail_server.state != 'AUTH': mail_server.close() mail_server.logout() # Dump the merged calendar to our output destination if options.output: output = open(options.output, 'wt') try: output.write(str(merged_calendar)) finally: output.close() else: print str(merged_calendar) return 0 if __name__ == '__main__': try: exit_code = main() except Exception, err: print >>sys.stderr, 'ERROR: %s' % str(err) exit_code = 1 sys.exit(exit_code)
Since I wanted to set up the export job to run on a regular basis via cron, I chose a command line interface. The main() function for mailbox2ics.py starts out at line 24 with the usual sort of configuration for command line option processing via the optparse module. Listing 3 shows the help output produced when the program is run with the -h option.
Usage: mailbox2ics.py [options] hostname username mailbox [mailbox...] Options: -h, --help show this help message and exit -p PASSWORD, --password=PASSWORD Password for username --port=PORT Port for IMAP server -v, --verbose Show progress -q, --quiet Do not show progress -o OUTPUT, --output=OUTPUT Output file
The --password option can be used to specify the IMAP account password on the command line, but if you choose to use it consider the security implications of embedding a password in the command line for a cron task or shell script. No matter how you specify the password, I recommend creating a separate mailbox2ics account on the IMAP server and limiting the rights it has so no data can be created or deleted and only public folders can be accessed. If --password is not specified on the command line, the user is prompted for a password when they run the program. While less useful with cron, providing the password interactively can be a solution if you are unable, or not allowed, to create a separate restricted account on the IMAP server. The account name used to connect to the server is required on the command line.
There is also a separate option for writing the ICS output data to a file. The default is to print the sequence of events to standard output in ICS format. Though it is easy enough to redirect standard output to a file, the -o option can be useful if you are using the -v option to enable verbose progress tracking and debugging.
The program uses a separate Calendar instance, merged_data, to hold all of the ICS information to be included in the output. All of the VEVENT components from the input are copied to merged_data in memory, and the entire calendar is written to the output location at the end of the program. After initialization (line 64), merged_data is configured with some basic properties. PRODID is required and specifies the name of the product which produced the ICS file. CALSCALE defines the date system, or scale, used for the calendar.
After setting up merged_calendar, mailbox2ics connects to the IMAP server. It tests whether the user has specified a network port using --port and only passes a port number to imaplib if the user includes the option. The optparse library converts the option value to an integer based on the option configuration, so options.port is either an integer or None.
The names of all mailboxes to be scanned are passed as arguments to mailbox2ics on the command line after the rest of the option switches. Each mailbox name is processed one at a time, in the for loop starting on line 79. After calling select() to change the IMAP context, the message ids of all of the messages in the mailbox are retrieved via a call to search(). The full content of each message in the mailbox is fetched in turn, and parsed with email.message_from_string(). Once the message has been parsed, the msg variable refers to an instance of email.Message.
Each message may have multiple parts containing different MIME encodings of the same data, as well as any additional message information or attachments included in the email which generated the event. For event notification messages, there is typically at least one human-readable representation of the event and frequently both HTML and plain text are included. Of course, the message also includes the actual ICS file, as well. For my purposes, only the ICS attachments were important, but there is no way to predict where they will appear in the sequence of attachments on the email message. To find the ICS attachments, mailbox2ics walks through all of the parts of the message recursively looking for attachments with mime-type text/calendar (as specified in the iCalendar standard) and ignoring everything else. Attachment names are ignored, since mime-type is a more reliable way to identify the calendar data accurately.
for part in msg.walk(): if part.get_content_type() == 'text/calendar': # Parse the calendar attachment ics_text = part.get_payload(decode=1) importing = Calendar.from_string(ics_text)
When it finds an ICS attachment, mailbox2ics parses the text of the attachment to create a new Calendar instance, then copies the VEVENT components from the parsed Calendar to merged_calendar. The events do not need to be sorted into any particular order when they are added to merged_calendar, since the client reading the ICS file will filter and reorder them as necessary to displaying them on screen. It was important to take the entire event, including any subcomponents, to ensure that all alarms are included. Instead of traversing the entire calendar and accessing each component individually, I simply iterated over the subcomponents of the top-level VCALENDAR node. Most of the ICS files only included one VEVENT anyway, but I did not want to miss anything important if that ever turned out not to be the case.
for event in importing.subcomponents: if event.name != 'VEVENT': continue merged_calendar.add_component(event)
Once all of the mailboxes, messages, and calendars are processed, the merged_calendar refers to a Calendar instance containing all of the events discovered. The last step in the process, starting at line 119, is for mailbox2ics to create the output. The event data is formatted using str(merged_calendar), just as in the example above, and written to the output destination selected by the user (standard output or file).
Listing 4 includes sample output from running mailbox2ics to merge two calendars for a couple of telecommuting workers, Alice and Bob. Both Alice and Bob have placed their calendars online at imap.example.com. In the output of mailbox2ics, you can see that Alice has 2 events in her calendar indicating the days when she will be in the office. Bob has one event for the day he has a meeting scheduled with Alice.
$ mailbox2ics.py -o group_schedule.ics imap.example.com mailbox2ics "Calendars.Alice" "Calendars.Bob" Password: Logging in to "imap.example.com" as mailbox2ics Scanning Calendars.Alice ... Found: In the office to work with Bob on project proposal Found: In the office Scanning Calendars.Bob ... Found: In the office to work with Alice on project proposal
The output file created by mailbox2ics containing the merged calendar data from Alice and Bob’s calendars is shown in Listing 5. You can see that it includes all 3 events as VEVENT components nested inside a single VCALENDAR. There were no alarms or other types of components in the input data.
BEGIN:VCALENDAR CALSCALE:GREGORIAN PRODID:-//mailbox2ics//doughellmann.com// BEGIN:VEVENT CLASS:PUBLIC DTEND;VALUE=DATE:20070704 DTSTAMP:20070705T180246Z DTSTART;VALUE=DATE:20070703 LAST-MODIFIED:20070705T180246Z LOCATION: PRIORITY:5 SEQUENCE:0 SUMMARY:In the office to work with Bob on project proposal TRANSP:TRANSPARENT UID:9628812.1182888943029.JavaMail.root(a)imap.example.com END:VEVENT BEGIN:VEVENT CLASS:PUBLIC DTEND;VALUE=DATE:20070627 DTSTAMP:20070625T154856Z DTSTART;VALUE=DATE:20070626 LAST-MODIFIED:20070625T154856Z LOCATION:Atlanta PRIORITY:5 SEQUENCE:0 SUMMARY:In the office TRANSP:TRANSPARENT UID:11588018.1182542267385.JavaMail.root(a)imap.example.com END:VEVENT BEGIN:VEVENT CLASS:PUBLIC DTEND;VALUE=DATE:20070704 DTSTAMP:20070705T180246Z DTSTART;VALUE=DATE:20070703 LAST-MODIFIED:20070705T180246Z LOCATION: PRIORITY:5 SEQUENCE:0 SUMMARY:In the office to work with Alice on project proposal TRANSP:TRANSPARENT UID:9628812.1182888943029.JavaMail.root(a)imap.example.com END:VEVENT END:VCALENDAR
To solve my original problem of merging the events into a sharable calendar to which I could subscribe in iCal, I scheduled mailbox2ics to run regularly via cron. With some experimentation, I found that running it every 10 minutes caught most of the updates quickly enough for my needs. The program runs locally on a web server which has access to the IMAP server. For better security, it connects to the IMAP server as a user with restricted permissions. The ICS output file produced is written to a directory accessible to the web server software. This lets me serve the ICS file as static content on the web server to multiple subscribers. Access to the file through the web is protected by a password, to prevent unauthorized access.
Mailbox2ics does everything I need it to do, for now. There are a few obvious areas where it could be enhanced to make it more generally useful to other users with different needs, though. Input and output filtering for events could be added. Incremental update support would help it scale to manage larger calendars. Handling non-event data in the calendar could also prove useful. And using a configuration file to hold the IMAP password would be more secure than passing it on the command line.
At the time of this writing, mailbox2ics does not offer any way to filter the input or output data other than by controlling which mailboxes are scanned. Adding finer-grained filtering support could be useful. The input data could be filtered at two different points, based on IMAP rules or the content of the calendar entries themselves.
IMAP filter rules (based on sender, recipient, subject line, message contents, or other headers) would use the capabilities of IMAP4.search() and the IMAP server without much effort on my part. All that would be needed are a few command line options to pass the filtering rules, or code to read a configuration file. The only difference in the processing by mailbox2ics would be to convert the input rules to the syntax understood by the IMAP server and pass them to search().
Filtering based on VEVENT properties would require a little more work. The event data must be downloaded and checked locally, since the IMAP server will not look inside the attachments to check the contents. Filtering using date ranges for the event start or stop date could be very useful, and not hard to implement. The Calendar class already converts dates to datetime instances. The datetime package makes it easy to test dates against rules such as “events in the next 7 days” or “events since Jan 1, 2007”.
Another simple addition would be pattern matching against other property values such as the event summary, organizer, location, or attendees. The patterns could be regular expressions, or a simpler syntax such as globbing. The event properties, when present in the input, are readily available through the __getitem__() API of the Calendar instance and it would be simple to compare them against the pattern(s).
If a large amount of data is involved, either spread across several calendars or because there are a lot of events, it might also be useful to be able to update an existing cached file, rather than building the whole ICS file from scratch each time. Looking only at unread messages in the folder, for example, would let mailbox2ics skip downloading old events that are no longer relevant or already appear in the local ICS file. It could then initialize merged_calendar by reading from the local file before updating it with new events and re-writing the file. Caching some of the results in this way would place less load on the IMAP server, so the export could easily be run more frequently than once every 10 minutes.
In addition to filtering to reduce the information included in the output, it might also prove useful to add extra information by including component types other than VEVENT. For example, including VTODO would allow users to include a group action list in the group calendar. Most scheduling clients support filtering the to-do items and alarms out of calendars to which you subscribe, so if the values are included in a feed, individual users can always ignore the ones they choose.
As mentioned earlier, using the --password option to provide the password to the IMAP server is convenient, but not secure. For example, on some systems it is possible to see the arguments to programs using ps. This allows any user on the system to watch for mailbox2ics to run and observe the password used. A more secure way to provide the password is through a configuration file. The file can have filesystem permissions set so that only the owner can access it. It could also, potentially, be encrypted, though that might be overkill for this type of program. It should not be necessary to run mailbox2ics on a server where there is a high risk that the password file might be exposed.
Mailbox2ics was a fun project that took a me just a few hours over a weekend to implement and test. This project illustrates two reasons why I enjoy developing with Python. First, difficult tasks are made easier through the power of the “batteries included” nature of Python’s standard distribution. And second, coupling Python with the wide array of other open source libraries available lets you get the job done, even when the Python standard library lacks the exact tool you need. Using the ICS file produced by mailbox2ics, I am now able to access the calendar data I need using my familiar tools, even though iCalendar is not supported directly by the group’s calendar server.