Skip to content

Instantly share code, notes, and snippets.

@PatrickChildersIT
Last active November 29, 2022 02:24
Show Gist options
  • Select an option

  • Save PatrickChildersIT/d1fa14b66fa84647b5c2944bdada7134 to your computer and use it in GitHub Desktop.

Select an option

Save PatrickChildersIT/d1fa14b66fa84647b5c2944bdada7134 to your computer and use it in GitHub Desktop.
A simple python 3 script to use microsoft outlook's COM api via pythoncom (pypiwin32 package) to map, depth first, the organizational structure as tracked in exchange.
import pythoncom
import win32com.client
class Contact():
"""Class serves as little more than an enhanced node tree."""
def __init__(self, exchange_user):
"""
Establish a core COM object to work off of for subsequent properties.
Manager and workers are cached, in that they are only gotten once.
This reduces external calls if we need to access the name or job title multiple times.
"""
self._exchange_user = exchange_user
self._manager = None
self._workers = None
@property
def name(self):
"""
It is the responsability of the organization controlling the exchange server communicated to by Outlook
to fill in details such as Name and Job Title.
"""
return self._exchange_user.Name
@property
def title(self):
"""
It is the responsability of the organization controlling the exchange server communicated to by Outlook
to fill in details such as Name and Job Title.
"""
return self._exchange_user.JobTitle
@property
def manager(self):
"""Getting an exchange user's manager is fairly easy, and wrapping each in our class is relatively inexpensive."""
if self._manager is None:
self._manager = Contact(self._exchange_user.GetExchangeUserManager())
return self._manager
@property
def workers(self):
"""
Exchange Users can acquire workers by lists of address entries,
fortunately each address entry can effortlessly get its exchange user object counterpart.
A given address entry isn't guaranteed to be an Exchange User, normally, but in this case we're working
exclusively within Exchange, so the call to acquire an exchange user should not fail.
Still, though, stranger things have happened.
"""
if self._workers is None:
self._workers = []
for address_entry in self._exchange_user.GetDirectReports():
self._workers.append(Contact(address_entry.GetExchangeUser()))
return self._workers
def __repr__(self):
"""
Generic repr I've used elsewhere.
This probably should be changed to better reflect what we want our output to end up looking like.
"""
return f"{self.__class__.__name__}({self.name}, {self.title})"
def recurse_print_contacts(root_contact, depth=0):
"""
Depth first chosen because I already had the boilerplate code on hand.
Also, since this isn't a search then navigating the tree will take just as long if it were breadth first.
I believe Depth first implementations are easier to understand for any novices, so that helps too.
"""
# I like seeing the list of users flow through to give me an idea of the speed the program is going.
# Also since external service calls are the speed bottleneck and not console output this isn't significant.
print(root_contact,depth)
this_repr = "{0}{1} ({2})\n".format('\t'*depth, root_contact.name, root_contact.title)
for worker in root_contact.workers:
this_repr += recurse_print_contacts(worker, depth+1)
return this_repr
# Allow win32com to generate python code files for Outlook. They're imperfect but serve their purpose fairly well.
generate_cache = win32com.client.gencache.EnsureDispatch("Outlook.Application")
outlook_namespace = win32com.client.DispatchEx("Outlook.Application").Session
# The first account, I assume, is the primary account of the exchange mailbox owner using Outlook.
# After that navigating properties is trivial. Finding the current user's Address Entry probably could've been done better/quicker,
# but this is the first series of properties I found that lead me to what I wanted.
my_address_entry = outlook_namespace.Accounts.Item(1).CurrentUser.AddressEntry
this_contact = Contact(my_address_entry.GetExchangeUser())
CEO_name = "Smith, John" # whatever the root user's name of the tree is in Exchange
# Here we hope that someone above us in the organization has the name we specified, or this while loop isn't going to end.
while this_contact.name != CEO_name:
this_contact = this_contact.manager
#this_contact is now the contact for the CEO (or root of the hierarchy we're going to print out)
whole_repr = recurse_print_contacts(this_contact)
print(whole_repr)
@PatrickChildersIT
Copy link
Author

PatrickChildersIT commented May 7, 2019

Note, that this "finds" the CEO (or otherwise root of the tree), not by searching the exchange global address list (GAL), but by navigating up the hierarchy starting from the running user's account. A GAL search would be better, and faster, but I found the documentation for these functions first, and I found this method easy to implement quickly. If the given name is not above the current user in the hierarchy this will loop forever.
Also note that each call of GetExchangeUserManager or GetExchangeUser I believe results in a call to the exchange server.
This is the bottleneck in performance, as a large org tree could take quite some time to complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment