Skip to content

Instantly share code, notes, and snippets.

@MickeyPvX
Created September 12, 2018 13:40
Show Gist options
  • Save MickeyPvX/186b1ed24a206004babd5b239c9182b8 to your computer and use it in GitHub Desktop.
Save MickeyPvX/186b1ed24a206004babd5b239c9182b8 to your computer and use it in GitHub Desktop.
Interact with MS Outlook: download attachments, create MailItems
import os
import pandas as pd
from comtypes.client import CreateObject, GetActiveObject
class OutlookInstance():
"""Class to help with MS Outlook interoperability
Attributes:
outlook (comtypes.POINTER(_Application)): MS Outlook Application
namespace (comtypes.POINTER(_NameSpace)): Namespace for outlook; Default = 'MAPI'
mailbox (comtypes.POINTER()): Mailbox for target Outlook account
targetfolder (comtypes.POINTER()): Outlook folder in mailbox to search
dlfolder (str): Folder path to which to download attachments
reportdata (dict): Dictionary of pandas DataFrames containing data from selected attachments
dlerrors (list): List of errors that occurred during attachment downloading
delfolder (comtypes.POINTER()): Deleted Items folder for mailbox
Methods:
set_namespace: Sets the namespace for outlook object
set_mailbox: Sets the mailbox in namespace, if present.
set_target_folder: Sets the targetfolder in mailbox
download_attachment_data: downloads specified attachments to dlfolder, retains data to reportdata dictionary
create_mail: Create an email item to modify or send automatically
close: dispose of the OutlookInstance object; close Outlook Application
"""
class Oli():
"""Iterator for objects in the Outlook object. Credit to 'Martin Stancik':
https://stackoverflow.com/questions/5077625/reading-e-mails-from-outlook-with-python-through-mapi
Attributes:
_obj (outlook_object): object from an outlook application that contains items
Methods:
items: Yields the items for _obj
prop: Gets properties for the items in _obj
"""
def __init__(self, outlook_object):
self._obj = outlook_object
def items(self):
array_size = self._obj.Count
for item_index in range(1, array_size + 1):
yield (item_index, self._obj[item_index])
def prop(self):
return sorted(self._obj._prop_map_get_.keys())
def __enter__(self):
return self
def __init__(self, mailbox, namespace='MAPI', tgtfolder=None):
"""Initializes the object
Args:
mailbox (str): Name of the mailbox to use
namespace (str): Namespace to use in Outlook; Default = 'MAPI'
tgtfolder (str): Name of the folder in mailbox to search; Default = None
"""
try:
self.outlook = GetActiveObject('Outlook.Application')
except:
self.outlook = CreateObject('Outlook.Application')
self.namespace = self.outlook.GetNamespace(namespace)
self.set_mailbox(mailbox)
if tgtfolder:
self.set_target_folder(tgtfolder)
self.dlfolder = os.path.abspath('.')
self.reportdata = {}
self.dlerrors = []
print('Outlook Instance initialized.')
def set_namespace(self, namespace):
"""Sets the NameSpace for the Outlook Application
Args:
namespace (str): NameSpace to search for
"""
self.namespace = self.outlook.GetNamespace(namespace)
def set_mailbox(self, mailbox):
"""Sets the mailbox to use in the Outlook object
Args:
mailbox (str): Mailbox to search for
"""
found = False
for _, mbox in self.Oli(self.namespace.Folders).items():
if mailbox.upper() in mbox.Name.upper():
self.mailbox = mbox
found = True
break
if not found:
print('Mailbox: {} not found!'.format(mailbox))
def set_target_folder(self, search_folder):
"""Sets the target folder in the mailbox
Args:
search_folder (str): Name of the folder to search for
Methods:
list_folders: recursively searches for the target folder, returns it when found
"""
def list_folders(olObject,target):
"""Recursive function to search all folders in an Outlook object for a target folder
Args:
olObject (comtypes.POINTER()): Object containing folders to be searched
target (str): Name of the folder to search for
Returns:
found_folder (comtypes.POINTER()): Folder with name matching target
"""
nonlocal found_folder
for _, folder in olObject.items():
if found_folder is None:
if folder.Name.upper() == target.upper():
found_folder = folder
elif 'Deleted Items' in folder.Name:
self.delfolder = folder
list_folders(self.Oli(folder.Folders), target)
else:
list_folders(self.Oli(folder.Folders), target)
else:
return found_folder
found_folder = None
list_folders(self.Oli(self.mailbox.Folders), search_folder)
if found_folder is None:
print('Folder {} not found in mailbox {}!'.format(search_folder, self.mailbox.Name))
else:
self.targetfolder = found_folder
def download_attachment_data(self, subject_list, reports_list, dlfolder=None, filetypes='excel',
sheet_names=None, headers=0, del_emails=False, del_files=False):
"""Downloads attachments in reports_list from emails with subjects in subject_list to dlfolder, retains data from
attachments in report_data dictionary.
Args:
subject_list (list): Email subjects containing attachments to download
reports_list (list): Names of attachments to download
dlfolder (str): File folder where attachments are downloaded; Default = None
filetypes (str): Types of files to download; currently only supporting Excel attachments; Default = 'excel'
sheet_names (list): Names of worksheets in Excel to read; Default = None
headers (int): Location of data headers in the attachments; Default = 0
del_emails (bool): Whether to delete the emails after downloading attachments; Default = False
del_files (bool): Whether to retain the downloaded files after extracting data from them; Default = False
"""
if not self.targetfolder:
print('Target Folder not set; run set_target_folder().')
return 0
else:
self.dlerrors = []
if not dlfolder:
dlfolder = self.dlfolder
if not os.path.isdir(dlfolder):
os.mkdir(dlfolder)
itemslist = list(self.Oli(self.targetfolder.Items).items())
todelete = []
for i in range(len(itemslist) - 1, -1, -1):
item = itemslist[i][1]
if item.Subject is None:
continue
if any(x.upper() in item.Subject.upper() for x in subject_list):
attachments = item.Attachments
for j in range(attachments.Count):
att = attachments.Item(j + 1)
if any(y.upper() in att.FileName.upper() for y in reports_list):
try:
fullpath = os.path.join(dlfolder, att.FileName)
if not os.path.isfile(fullpath):
att.SaveAsFile(fullpath)
print('{} Saved successfully'.format(att.FileName))
else:
print('{} Already downloaded'.format(att.FileName))
key, _ = att.FileName.split('.')
if filetypes == 'excel':
self.reportdata[key] = pd.read_excel(fullpath,
sheet_name=sheet_names,
header=headers)
else:
print('File Type loading not yet implemented.')
if del_files:
os.remove(fullpath)
except Exception as e:
self.dlerrors.append((att.FileName, e.args))
continue
if del_emails:
todelete.append(item.Subject)
item.Delete()
delitemslist = list(self.Oli(self.delfolder.Items).items())
for j in range(len(delitemslist) - 1, -1, -1):
if delitemslist[j][1].Subject in todelete:
delitemslist[j][1].Delete()
def create_mail(self, to=None, subject=None, body=None, html_body=None, att_path=None, auto_send=False):
"""Creates an Outlook MailItem
Args:
to (list/str): List of email addresses to add to the To: list
subject (str): Subject line of the email
body (str): Body of the email
html_body (str): Body of the email in HTML
att_path (str): Path to the file to attach
auto_send (bool): Whether to send the email automatically or display the item to be modified further
"""
olMailItem = 0x0
item = self.outlook.CreateItem(olMailItem)
if isinstance(to, list):
str_to = '; '.join(to)
item.To = str_to
elif isinstance(to, str):
item.To = to
else:
raise TypeError('To: must be a string or list of strings')
item.Subject = subject
item.Body = body
if att_path:
item.Attachments.Add(att_path)
if auto_send:
# item.Display()
item.Send()
else:
item.Display()
def close(self):
self.namespace.Logoff()
self.outlook.Quit()
self.namespace = None
self.outlook = None
def __exit__(self, *args, **kwargs):
self.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment