# --------------------------------------------------------------------------------- # Author: Dean Stringer # Description/Purpose: # # Command line script to fetch email objects from a back-end Exchange # store from a specific folder (and/or subfolder) extracting message # bodies into individual TXT files or a single concatenated one. # # Uses Win32::OLE to make MAPI (Messaging Application Programming Interface) # calls via the local Exchange client i/f on the machine on which the # script is run, which will more than likely be a Windows box ;-) # # Script assumes you are already logged in to your Exchange client, # and uses OLE to talk through that. You can pass a password into # the $session->Logon call though. With some versions of Outlook # you'll get a popup dialogue when you run this script asking you # to authorise access to the i/f for a specific period of time. # # This script is read-only but methods are available to delete, sort, # logon, logoff, getfirst, getlast, getnext, send, update etc etc ... # # So you could technically extend this script to act as a full interactive # command-line mail client. BUT any modifications/deletions you carry out # will be reflected in your personal Exchange server store so would be # best to develop using a local PST file/folder. # # !!! NOTE !!! you should let the script complete its operation # and not interupt it (CTRL-C) coz on one occasion (out of hundreds) # I had Outlook complain about a folder being damaged, but after # closing and restarting Outlook I was able to access its contents # without any loss of data # # Message retrieval (as performed by this script) doesnt seem to set the # message as having been read, it looks like you need to specifically # clear the 'unread' flag for the message # # See the following file (should be with this script) for a reference to the... # OLE_Messaging_Library.html # # --------------------------------------------------------------------------------- # There's a total absence of documentation for Perl use of OLE to do this # sort of thing, no suprise really as there's probably not much call for # Perl based command-line access to Outlook at Redmond, but fortunately # the following Usenet post outlined the basics.. # # From: Aaron (aibrahim1@ureach.com) # Subject: Problems accessing calendar items using Win32::OLE and CDO 1.2.1 # Newsgroups: comp.lang.perl.misc # Date: 2002-02-14 08:23:18 PST # --------------------------------------------------------------------------------- use strict; use Win32::OLE; # Access CDO through OLE Automation interface use Win32::OLE::Variant; # Need for display of 'Win32::OLE::Variant=SCALAR' types # like Dates and Times my $outputFolder = "archives\\"; my $outputMode = 'concat'; # if 'concat' write all messages out to one file my $outFileName = 'msgs.txt'; # only used in 'concat' mode my $contentMode = 'txt'; # 'txt' or 'html' output my $maxMessageSize = 10000000; # Max size in Bytes my $maxMessageCount = 2000; # Max num of messages we expect to handle my $mailboxName = "sent 2004"; # my $mailboxName = "Mailbox - Dean Stringer"; # default top-level 'infostore' name/label. Typically something like "Mailbox - Fred Dagg" # but may change over time, and be specific to a local outlook client config for the # user. my $folderName = 'Work'; # Typically something like 'Inbox' if you're after email, but you can use MAPI to get # at other stores too, like the Calendar, Contacts, Outbox, 'Sent Items' etc etc # each has different methods for handling its children my $subFolderName = ''; # e.g. 'Spam'. If blank extracts contents of $folderName my $passwd = ""; # not requried if user currently logged in with Outlook client my $profileName = 'Outlook'; # this is the exchange profile name entered when client # 1st setup, so if only one profile is present it will # probably by default be... # 'MS Exchange Settings' or maybe 'Outlook' # ---------------------------------------------------------------------------- # Try creating Session # ---------------------------------------------------------------------------- Win32::OLE->Initialize(Win32::OLE::COINIT_OLEINITIALIZE); my $session = Win32::OLE->new("MAPI.Session") or die "Can't create session!!!"; Win32::OLE->LastError(); my $err = $session->Logon($profileName, $passwd); my $infostores = $session->InfoStores(); # public folders, personal folders etc my $infostoreItem; my $mailboxInfostore; my $mailboxInfostoreID; # ---------------------------------------------------------------------------- # Loop through available infostores # ---------------------------------------------------------------------------- for (my $i = 1; $i <= $infostores->{COUNT}; $i++) { $infostoreItem = $infostores->Item($i); if ($infostoreItem->{NAME} eq $mailboxName) { $mailboxInfostore = $infostoreItem; $mailboxInfostoreID = $infostoreItem->{ID}; print "\nFound Mailbox: $infostoreItem->{NAME}"; last; } } die "\nNo MailBox found matching: $mailboxName" unless ($mailboxInfostoreID); my $rootFolder = $mailboxInfostore->{ROOTFOLDER}; # ---------------------------------------------------------------------------- # Find 1st-level container folder (e.g. InBox) # ---------------------------------------------------------------------------- my ( $mailboxFolderID, # top-level Infostore ID $topFolder, # the actual folder $targetFolderID # the sub-folder we're actually looking for ); for (my $i = 1; $i <= $rootFolder->Folders->{COUNT}; $i++) { if ($rootFolder->Folders($i)->{NAME} eq $folderName) { $mailboxFolderID = $rootFolder->Folders($i)->{ID}; $topFolder = $rootFolder->Folders($i); print "\nFound Folder: $folderName"; last; } } die "\nSubfolder $folderName not found. Did U get the case right?" unless ($mailboxFolderID); if ($subFolderName) { # ---------------------------------------------------------------------------- # Find sub-folder within top level folder (e.g. 'Spam') # ---------------------------------------------------------------------------- if ($mailboxFolderID) { for (my $i = 1; $i <= $topFolder->Folders->{COUNT}; $i++) { if ($topFolder->Folders($i)->{NAME} eq $subFolderName) { $targetFolderID = $topFolder->Folders($i)->{ID}; last; } } } die "\nSub-Folder '$subFolderName' not found." unless ($targetFolderID); } else { $targetFolderID = $mailboxFolderID; } # ---------------------------------------------------------------------------- # Check each message in folder # ---------------------------------------------------------------------------- my $targetFolder = $session->GetFolder($targetFolderID, $mailboxInfostore->{ID}); my $numItems = $targetFolder->Messages->{COUNT}; die "\nToo many messages: $numItems" unless ($numItems < $maxMessageCount); my $messageID; my $currentMessage; if ($outputMode eq 'concat') { open (OUTFILE, ">$outputFolder$outFileName"); } for (my $i = 1; $i <= $targetFolder->Messages->{COUNT}; $i++) { $messageID = $targetFolder->Messages($i)->{ID}; $currentMessage = $session->GetMessage($messageID); if ($currentMessage->{SIZE} > $maxMessageSize) { print "\nOVERSIZE"; } else { print "\n$currentMessage->{SUBJECT}"; # progress indication my $content = ""; my $recipients = ""; # Recipients isnt a simple list, theyre stored as 'AddressEntry' child objects for (my $r = 1; $r <= $currentMessage->{RECIPIENTS}->{COUNT}; $r++) { my $thisRecipient = $currentMessage->{RECIPIENTS}->Item($r); my $thisAddressEntry = $thisRecipient->AddressEntry; my $thisRecipientStr = $thisAddressEntry->Name; if ($thisRecipientStr =~ /(.*)CN=(.+)$/) { # if an outlook address (LDAP?) entry, try and extract the common name? off then end $thisRecipientStr = $2; # example in this format... # /O=MY COMPANY NAME/OU=MYCOMP/CN=RECIPIENTS/CN=JoeBloggs # there is an AddressType attribute but it doesnt seem very helpful } $recipients .= $thisRecipientStr . ", "; } if ($contentMode eq 'html') { # if we want to write in a nice web-viewable form $content = "#title \"" . $currentMessage->{SUBJECT} . "\"\n" . "#exchangeMessageID \"" . $currentMessage->{ID} . "\"\n" . "

From: " . $currentMessage->{SENDER}->{NAME} . "\n" . "
Sent: " . $currentMessage->{TIMESENT} . "\n" . "
Subject: " . $currentMessage->{SUBJECT} . "\n" . "
" . $currentMessage->{TEXT} . "\n"; } else { # plain text output $content = "\n\n" . "From: " . $currentMessage->{SENDER}->{NAME} . "\n" . "Sent: " . $currentMessage->{TIMESENT} . "\n" . "Recipients: " . $recipients . "\n" . "Subject: " . $currentMessage->{SUBJECT} . "\n" . "" . $currentMessage->{TEXT} . "\n"; } if ($outputMode eq 'concat') { # single archive file print OUTFILE $content; } else { # save each message in an individual file # 'ID' is a unique variable-length string that is the message identifier, # the right most 11 characters of which we'll use for the filename (as these # seem to be all that are changing, I guess the rest are related to the users # account or server ID, plus we'll write out the full ID to the file body my $outputFilename = substr($currentMessage->{ID},(length($currentMessage->{ID}) - 11)); open (OUTFILE, ">$outputFolder$outputFilename.txt"); print OUTFILE $content; close (OUTFILE); } # There are loads of other top-level attributes and objects. Check 'em out.. # foreach my $key (keys %{$currentMessage}) { # print $key . "=" . $currentMessage->{$key} . "\n"; #} } } if ($outputMode eq 'concat') { close (OUTFILE); } # ---------------------------------------------------------------------------- # Garbage Collection.... # ---------------------------------------------------------------------------- $session->Logoff(); undef $mailboxInfostore; undef $infostores; undef $rootFolder; undef $topFolder; undef $infostoreItem; undef $currentMessage;