Last active
November 29, 2022 02:24
-
-
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.
This file contains hidden or 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
| 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) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
GetExchangeUserManagerorGetExchangeUserI 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.