0

I am wanting to make a outlook monitor in Python and have been researching on the topic.

I am using Python 3.6 to try and monitor email received in a shared inbox on my outlook. I have the following code which will run:

import win32com.client
import ctypes # for the VM_QUIT to stop PumpMessage()
import pythoncom
import re
import time
import psutil
import os

from os.path import join as join_path
from olefile import OleFileIO


class HandlerClass():
    def __init__(self):
        # First action to do when using the class in the DispatchWithEvents
        messages = self  # Getting All Messages in the Inbox Folder
        #headers_schema = "http://schemas.microsoft.com/mapi/proptag/0x007D001E"

        # Check for unread emails when starting the event
        for message in messages:
            if message.UnRead:
                self.check_email(message)
            else:
                print("No Unread Emails")
                break

    def check_email(self, message):
        attachments = message.Attachments  # Get attachments
        if attachments.Count > 0:  # If one or more attachments
            attachment = attachments.Item(1)  # Get the first attachment
            attachment_fname = join_path(os.getcwd(), attachment.FileName)
            attachment.SaveAsFile(attachment_fname)  # Saves to the attachment to current folded
            try:
                IHDict = extract_headers(attachment_fname)
            except MessageHeadersError as err:
                print("Could not extract headers. Is this an email?", err)
                return
            finally:
                os.remove(attachment_fname)  # Delete saved attachment

            def check_header(h: str) -> bool:
                hval = IHDict.get(h, None)
                if hval is not None:
                    print("%s: %s" % (h, hval))
                else:
                    print("'%s' not available." % h)
                return hval

            # Pull out headers of interest
            rp = check_header('Return-Path')
            sp = check_header('X-Env-Sender')
            check_header('X-Originating-Ip')
            check_header('Authentication-Results')
            print()  # Formatting

            # Compare Return Path to X-Env-Sender to check for Phish.
            if rp is not None and sp is not None and rp.strip("<>") == sp:
                print("Email is safe.")
                print() # Formatitng
            else:
                print("Email is suspicious.")
                print() # Formatting
        else:
            print("Email Contains No Attachment Message")

    def OnQuit(self):
        # To stop PumpMessages() when Outlook Quit
        # Note: Not sure it works when disconnecting!!
        ctypes.windll.user32.PostQuitMessage(0)

    def OnItemAdd(self, mail):
        print("Working!")
        #self.check_email(mail)
        #Check if the item is of the MailItem type
        if mail.Class==43:
            print(mail.Subject, " - ", mail.Parent.FolderPath)



# Function to check if outlook is open
def check_outlook_open():
    for pid in psutil.pids():
        p = psutil.Process(pid)
        if p.name().lower().strip() == 'outlook.exe':
            return True
    return False


class MessageHeadersError(Exception):
    pass


class MessageHeadersMissing(MessageHeadersError):
    pass


class MessageHeadersUnknownEncoding(MessageHeadersError):
    pass


def extract_headers(cdfv2_filename: str) -> dict:
    """Extract headers from a CDFv2 email"""
    try:
        ole = OleFileIO(cdfv2_filename)
    except Exception as exc:
        raise MessageHeadersError("could not open OLE file") from exc
    try:
        for ent in ole.listdir(streams=True, storages=False):
            if ent[-1].startswith("__substg1.0_007D"):
                # message headers 
                break
        else:
            # no message header entry?
            raise MessageHeadersMissing("missing")

        olestream = ole.openstream(ent)
        header_data = olestream.read()
        olestream.close()

        if ent[-1].endswith("001E"):
            # ASCII encoding
            header_string = header_data.decode("ascii")
        elif ent[-1].endswith("001F"):
            # UTF-16
            header_string = header_data.decode("UTF-16")
        else:
            # dunno what this encoding is
            raise MessageHeadersUnknownEncoding("Unknown OLE encoding " + ent[-4:])

        return parse_headers(header_string)
    finally:
        ole.close()


def parse_headers(hstr: str) -> dict:
    headers = {}
    lastkey = None

    for line in hstr.split("\n"):
        #if line.strip() == " ": #skip empty lines - there shouldn't be any. continue 
        if line.startswith("\t") or line.startswith(" "): #headers can be continued with either whitespace or tabs.
            key = lastkey
            value = line.strip()
        else:
            key, _, value = line.partition(":")# e - mail headers are case insensitive, #so we normalise to title - case (the most common form).
            key = key.strip().title()
            value = value.strip()
            lastkey = headers[key] = (headers.get(key, "") + " " + value).strip()
    return headers


# Loop 
Syscoms = win32com.client.DispatchEx("Outlook.Application").GetNamespace("MAPI").Folders["UK"].Folders["Inbox"].Items
while True:
    # If outlook opened then it will start the DispatchWithEvents
    if check_outlook_open():
        win32com.client.DispatchWithEvents(Syscoms, HandlerClass)
        pythoncom.PumpMessages()
    # To not check all the time (should increase 10 depending on your needs)
    time.sleep(1)

How can I get the OnItemAdd event to trigger when a new mail arrives in the Inbox?

  • What you want is to trigger something when a new email arrives, but `OnItemAdd` does not work? – Ben.T May 11 '18 at 13:35
  • Yes this is indeed the case, I cannot get it to work no matter what I try - I think I'm missing a step. – ShatteredPheonix May 12 '18 at 02:09
  • I tried your code and I don't find how to trigger OnItemAdd with Item Event. Do you have a specific reason to use Item Event and not Application Event as in [this question](https://stackoverflow.com/questions/49695160/how-to-continuously-monitor-a-new-mail-in-outlook-and-unread-mails-of-a-specific/49778696#49778696) – Ben.T May 14 '18 at 15:06
  • It's because the I want to monitor a shared mailbox and as far as my understanding after looking at google and previous answers, it's not possible to use OnNewMail event, as this only works for default mailboxes and not shared. I have not tried Application event but I would like to get this working for each new email as it arrives rather than having it loop through the entire inbox every time. – ShatteredPheonix May 17 '18 at 19:40
  • Good point, sorry I forgot this information after focusing on ItemAdd. Anyway, I tried a bunch of things but no success good luck – Ben.T May 18 '18 at 16:58

1 Answers1

1

Try using Dispatch instead of DispatchEx and GetSharedDefaultFolder instead of Inbox. The following code properly registers incoming emails to the shared inbox:

import ctypes # for the VM_QUIT to stop PumpMessage()
import pythoncom
import win32com.client
import sys


# outlook config
SHARED_MAILBOX = "Your Mailbox Name"

# get the outlook instance and inbox folder
session = win32com.client.Dispatch("Outlook.Application").Session
user = session.CreateRecipient(SHARED_MAILBOX)
shared_inbox = session.GetSharedDefaultFolder(user, 6).Items  # 6 is Inbox


class HandlerClass(object):

    def OnItemAdd(self, item):
        print("New item added in shared mailbox")
        if item.Class == 43:
            print("The item is an email!")


outlook = win32com.client.DispatchWithEvents(shared_inbox, HandlerClass)


def main():
    print("Starting up Outlook watcher")
    pythoncom.PumpMessages()


if __name__ == "__main__":
    try:
        status = main()
        sys.exit(status)
    except KeyboardInterrupt:
        print("Terminating program..")
        ctypes.windll.user32.PostQuitMessage(0)
        sys.exit()

EDIT:

There's a bug in my code posted above, and although it's not part of the question, I don't want to leave it like that. The program will not catch the KeyboardInterrupt like this, because PumpMessages is an infinite loop which blocks the main thread. Putting it into its own thread using the threading module gets around that:

class OtlkThread(threading.Thread):
    def run(self):
        logger.info("Starting up Outlook watcher\n"
                    "To terminate the program, press 'Ctrl + C'")
        pythoncom.PumpMessages()


def main():
    win32com.client.DispatchWithEvents(shared_inbox, HandlerClass)
    OtlkThread(daemon=True).start()


if __name__ == "__main__":
    status = main()
    while True:
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            logger.info("Terminating program..")
            ctypes.windll.user32.PostQuitMessage(0)
            sys.exit(status)
iuvbio
  • 600
  • 6
  • 23