-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tool Exchange Calendar Downloader (#19)
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
Showing
4 changed files
with
182 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
|
||
## Under development | ||
|
||
- | ||
- Tools: downloadExchange | ||
|
||
## Version 0.4 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |