Skip to content

Commit cc10d05

Browse files
Jack28Felix Bauer
andauthored
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]>
1 parent 5e860d8 commit cc10d05

File tree

4 files changed

+182
-1
lines changed

4 files changed

+182
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Under development
44

5-
-
5+
- Tools: downloadExchange
66

77
## Version 0.4
88

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,40 @@ It is strongly recommended to explicitly sync the calendar back to its source:
109109

110110
Send / Receive -> Send all
111111

112+
### A tool to download calendars from Microsoft Exchange
113+
114+
`tools/downloadExchange.py` and its configuration file `exchange.conf` can be
115+
used to download a calendar from Microsoft Exchange Server using a functional
116+
account that shares calendars with other users.
117+
118+
The benefit of this solution is the calendars remain inside Microsoft Exchange.
119+
If a backup is taken they are part of it, no additional service is needed, and
120+
access permissions are handled by Microsoft Exchange.
121+
122+
$ tools/downloadExchange.py -h
123+
usage: downloadExchange.py [-h] [-c CONFIG] [-v] [-s START_DATE] [-e END_DATE] [-t]
124+
125+
options:
126+
-h, --help show this help message and exit
127+
-c CONFIG, --config CONFIG
128+
Absolute or relative path to configuration file.
129+
-v, --verbose More v's more text
130+
-s START_DATE, --start-date START_DATE
131+
Start Date e.g. 2023-05-02. Default is todays date
132+
-e END_DATE, --end-date END_DATE
133+
End Date e.g. 2023-05-03. Default is start-date + 7 days. (00:00:00 respectively)
134+
-t, --test No Exchange server? Run script on dummy data!
135+
136+
The configuration file can/should contain the following options:
137+
138+
[exchange]
139+
user = MYWINDOMAIN\functional_account
140+
141+
password = secure_functional_password
142+
#calendar = Calendar
143+
#host = localhost
144+
#outfile = calendar_events.ics
145+
112146
### A scriptable tool to create events ###
113147

114148
Part of the package is a script `addEventToIcal.py` that helps migration from

tools/downloadExchange.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python
2+
# encoding: utf-8
3+
4+
"""
5+
A tool to download a calendar from Microsoft Exchange
6+
"""
7+
8+
import argparse
9+
import configparser
10+
import datetime
11+
import logging
12+
import dateutil.parser
13+
import exchangelib
14+
import icalendar
15+
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument('-c', '--config', default='./exchange.conf',
22+
help='Absolute or relative path to configuration file.')
23+
parser.add_argument('-v', '--verbose', action='count', default=0,
24+
help='More v\'s more text')
25+
parser.add_argument('-s', '--start-date', default=None,
26+
help='Start Date e.g. 2023-05-02. Default is todays date')
27+
parser.add_argument('-e', '--end-date', default=None,
28+
help='End Date e.g. 2023-05-03. ' +
29+
'Default is start-date + 7 days. ' +
30+
'(00:00:00 respectively)')
31+
parser.add_argument('-t', '--test', action='store_true',
32+
help='No Exchange server? Run script on dummy data!')
33+
args = parser.parse_args()
34+
35+
logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)s %(message)s',
36+
datefmt='%H:%M:%S',
37+
level=logging.ERROR)
38+
39+
if args.verbose == 1:
40+
logger.setLevel(logging.INFO)
41+
logger.info("Loglevel INFO")
42+
if args.verbose >= 2:
43+
logger.setLevel(logging.DEBUG)
44+
logger.info("Loglevel DEBUG")
45+
logger.debug("Parsed args: %s", args)
46+
47+
# log system settings
48+
logger.info("Datetime utc now: %s", datetime.datetime.utcnow())
49+
logger.info("Datetime local time now: %s", datetime.datetime.now().astimezone())
50+
51+
52+
# Read Config file with utf-8 encoding (Umlaute ä, ö, ü, ... can be read)
53+
config = configparser.ConfigParser()
54+
with open(args.config, mode='r', encoding='utf-8') as conf:
55+
config.read_file(conf)
56+
config = config['exchange']
57+
logger.debug("Read config %s", args.config)
58+
59+
60+
# prepare credentials for login
61+
credentials = exchangelib.Credentials(config['user'],
62+
config['password'])
63+
64+
xconfig = exchangelib.Configuration(server=config.get('host', 'localhost'),
65+
credentials=credentials)
66+
67+
if not args.test:
68+
# Connect to the Exchange server
69+
account = exchangelib.Account(
70+
primary_smtp_address=config['email'],
71+
config=xconfig,
72+
autodiscover=False,
73+
access_type=exchangelib.DELEGATE,
74+
)
75+
76+
selected_calendar = account.calendar
77+
if config.get('calendar', None):
78+
# walk calendars
79+
logger.info("Walk calendars")
80+
for cal_folder in account.calendar.children:
81+
logger.info("Found calendar: %s", cal_folder)
82+
if -1 !=str(cal_folder).find(config.get('calendar', 'Calendar')):
83+
selected_calendar = cal_folder
84+
break
85+
86+
if args.start_date:
87+
start = dateutil.parser.parse(args.start_date)
88+
else:
89+
start = datetime.datetime.today()
90+
if args.end_date:
91+
end = dateutil.parser.parse(args.end_date)
92+
else:
93+
end = start + datetime.timedelta(days=7)
94+
95+
# tzinfo object must be compatible with exchangelib
96+
# https://github.com/ecederstrand/exchangelib/issues/1076
97+
if args.test:
98+
tz = exchangelib.EWSTimeZone.localzone()
99+
else:
100+
tz = account.default_timezone
101+
102+
start = exchangelib.EWSDateTime.from_datetime(start).astimezone(tz)
103+
end = exchangelib.EWSDateTime.from_datetime(end).astimezone(tz)
104+
105+
logger.debug("Start date is: %s", start)
106+
logger.debug("End date is: %s", end)
107+
108+
# exchangelib.Account.calendar.all() can not be used because it doesn't expand
109+
# recurring events. exchangelib.CalendarItem['recurrence'] and
110+
# icalendar.Event['rrule'] are not in any way compatible or translate
111+
# meaningfully.
112+
if not args.test:
113+
calendar_items = selected_calendar.view(start=start, end=end)
114+
else:
115+
calendar_items = [exchangelib.CalendarItem(subject="foo1", start=start, end=end),
116+
exchangelib.CalendarItem(subject="foo2", start=start, end=end),
117+
exchangelib.CalendarItem(subject="bar1", start=start, end=end)]
118+
119+
# Create a new iCalendar object
120+
ical = icalendar.Calendar()
121+
122+
# Iterate through each calendar item and add it to the iCalendar object
123+
# ATTENTION!! Only summary, start, end, and description are copied
124+
for item in calendar_items:
125+
logger.debug("Read item: %s, %s, %s", item.subject, item.start, item.end)
126+
event = icalendar.Event()
127+
event.add('summary', item.subject)
128+
event.add('dtstart', item.start)
129+
event.add('dtend', item.end)
130+
event.add('description', item.body)
131+
# Add more properties as needed, such as location, attendees, etc.
132+
133+
ical.add_component(event)
134+
135+
# Save the iCalendar object to a file
136+
with open(config.get('outfile', 'calendar_events.ics'), 'wb') as f:
137+
f.write(ical.to_ical())
138+
139+
logger.info("Calendar events have been exported to %s",
140+
config.get('outfile', 'calendar_events.ics'))

tools/exchange.conf.sample

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[exchange]
2+
user = MYWINDOMAIN\functional_account
3+
4+
password = secure_functional_password
5+
#calendar = Calendar
6+
#host = localhost
7+
#outfile = calendar_events.ics

0 commit comments

Comments
 (0)