Skip to content

Commit

Permalink
Add tool Exchange Calendar Downloader (#19)
Browse files Browse the repository at this point in the history
This pull request adds a tool do download a calendar of events from Microsoft Exchange.

The events originate from a configurable calendar (account and calendar name) and specified day range:
$ tools/downloadExchange.py -s $(date +%Y-%m-%d --date="today") -e $(date +%Y-%m-%d --date="+2week")


exchangelib.Account.calendar.all() can not be used because it doesn't expand
recurring events.
exchangelib.CalendarItem['recurrence'] and icalendar.Event['rrule'] are not in
any way compatible or translate meaningfully.

---------

Co-authored-by: Felix Bauer <[email protected]>
  • Loading branch information
Jack28 and Felix Bauer authored Apr 17, 2024
1 parent 5e860d8 commit cc10d05
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Under development

-
- Tools: downloadExchange

## Version 0.4

Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,40 @@ It is strongly recommended to explicitly sync the calendar back to its source:

Send / Receive -> Send all

### A tool to download calendars from Microsoft Exchange

`tools/downloadExchange.py` and its configuration file `exchange.conf` can be
used to download a calendar from Microsoft Exchange Server using a functional
account that shares calendars with other users.

The benefit of this solution is the calendars remain inside Microsoft Exchange.
If a backup is taken they are part of it, no additional service is needed, and
access permissions are handled by Microsoft Exchange.

$ tools/downloadExchange.py -h
usage: downloadExchange.py [-h] [-c CONFIG] [-v] [-s START_DATE] [-e END_DATE] [-t]

options:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
Absolute or relative path to configuration file.
-v, --verbose More v's more text
-s START_DATE, --start-date START_DATE
Start Date e.g. 2023-05-02. Default is todays date
-e END_DATE, --end-date END_DATE
End Date e.g. 2023-05-03. Default is start-date + 7 days. (00:00:00 respectively)
-t, --test No Exchange server? Run script on dummy data!

The configuration file can/should contain the following options:

[exchange]
user = MYWINDOMAIN\functional_account
email = [email protected]
password = secure_functional_password
#calendar = Calendar
#host = localhost
#outfile = calendar_events.ics

### A scriptable tool to create events ###

Part of the package is a script `addEventToIcal.py` that helps migration from
Expand Down
140 changes: 140 additions & 0 deletions tools/downloadExchange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env python
# encoding: utf-8

"""
A tool to download a calendar from Microsoft Exchange
"""

import argparse
import configparser
import datetime
import logging
import dateutil.parser
import exchangelib
import icalendar


logger = logging.getLogger(__name__)


parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', default='./exchange.conf',
help='Absolute or relative path to configuration file.')
parser.add_argument('-v', '--verbose', action='count', default=0,
help='More v\'s more text')
parser.add_argument('-s', '--start-date', default=None,
help='Start Date e.g. 2023-05-02. Default is todays date')
parser.add_argument('-e', '--end-date', default=None,
help='End Date e.g. 2023-05-03. ' +
'Default is start-date + 7 days. ' +
'(00:00:00 respectively)')
parser.add_argument('-t', '--test', action='store_true',
help='No Exchange server? Run script on dummy data!')
args = parser.parse_args()

logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)s %(message)s',
datefmt='%H:%M:%S',
level=logging.ERROR)

if args.verbose == 1:
logger.setLevel(logging.INFO)
logger.info("Loglevel INFO")
if args.verbose >= 2:
logger.setLevel(logging.DEBUG)
logger.info("Loglevel DEBUG")
logger.debug("Parsed args: %s", args)

# log system settings
logger.info("Datetime utc now: %s", datetime.datetime.utcnow())
logger.info("Datetime local time now: %s", datetime.datetime.now().astimezone())


# Read Config file with utf-8 encoding (Umlaute ä, ö, ü, ... can be read)
config = configparser.ConfigParser()
with open(args.config, mode='r', encoding='utf-8') as conf:
config.read_file(conf)
config = config['exchange']
logger.debug("Read config %s", args.config)


# prepare credentials for login
credentials = exchangelib.Credentials(config['user'],
config['password'])

xconfig = exchangelib.Configuration(server=config.get('host', 'localhost'),
credentials=credentials)

if not args.test:
# Connect to the Exchange server
account = exchangelib.Account(
primary_smtp_address=config['email'],
config=xconfig,
autodiscover=False,
access_type=exchangelib.DELEGATE,
)

selected_calendar = account.calendar
if config.get('calendar', None):
# walk calendars
logger.info("Walk calendars")
for cal_folder in account.calendar.children:
logger.info("Found calendar: %s", cal_folder)
if -1 !=str(cal_folder).find(config.get('calendar', 'Calendar')):
selected_calendar = cal_folder
break

if args.start_date:
start = dateutil.parser.parse(args.start_date)
else:
start = datetime.datetime.today()
if args.end_date:
end = dateutil.parser.parse(args.end_date)
else:
end = start + datetime.timedelta(days=7)

# tzinfo object must be compatible with exchangelib
# https://github.com/ecederstrand/exchangelib/issues/1076
if args.test:
tz = exchangelib.EWSTimeZone.localzone()
else:
tz = account.default_timezone

start = exchangelib.EWSDateTime.from_datetime(start).astimezone(tz)
end = exchangelib.EWSDateTime.from_datetime(end).astimezone(tz)

logger.debug("Start date is: %s", start)
logger.debug("End date is: %s", end)

# exchangelib.Account.calendar.all() can not be used because it doesn't expand
# recurring events. exchangelib.CalendarItem['recurrence'] and
# icalendar.Event['rrule'] are not in any way compatible or translate
# meaningfully.
if not args.test:
calendar_items = selected_calendar.view(start=start, end=end)
else:
calendar_items = [exchangelib.CalendarItem(subject="foo1", start=start, end=end),
exchangelib.CalendarItem(subject="foo2", start=start, end=end),
exchangelib.CalendarItem(subject="bar1", start=start, end=end)]

# Create a new iCalendar object
ical = icalendar.Calendar()

# Iterate through each calendar item and add it to the iCalendar object
# ATTENTION!! Only summary, start, end, and description are copied
for item in calendar_items:
logger.debug("Read item: %s, %s, %s", item.subject, item.start, item.end)
event = icalendar.Event()
event.add('summary', item.subject)
event.add('dtstart', item.start)
event.add('dtend', item.end)
event.add('description', item.body)
# Add more properties as needed, such as location, attendees, etc.

ical.add_component(event)

# Save the iCalendar object to a file
with open(config.get('outfile', 'calendar_events.ics'), 'wb') as f:
f.write(ical.to_ical())

logger.info("Calendar events have been exported to %s",
config.get('outfile', 'calendar_events.ics'))
7 changes: 7 additions & 0 deletions tools/exchange.conf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[exchange]
user = MYWINDOMAIN\functional_account
email = [email protected]
password = secure_functional_password
#calendar = Calendar
#host = localhost
#outfile = calendar_events.ics

0 comments on commit cc10d05

Please sign in to comment.