Narrowing down / speeding up search in extracted outlook shcedule - excel

I am trying to find a specific subject in a shared outlook calendar on a specific date. The subject, the date and the shared calendar is passed as arguments. The script below works (I simplified it a bit for readability in this thread). BUT it is extremely slow since the "for" and "if" statement goes through all the schedules in all the dates. I got about 20 shared calendars to go through over 15 days time period; equating to about 300 times that the function is called (300 cells) in excel. This takes a huge amount of time to process, like an hour or or so. I speeded it up a little by exiting the "for" loop as soon as a match is found. But for those dates when there is no match, the for loop has to go through all the calendar item. And some calendar has huge number of schedules. Is there any way to actually only extract the schedules on the specified date, leaving the "for" loop to go through only handful of schedules on that day? Any help would be appreciated.
Function FindAttendance(xDate As Date, xSubject As String, xEmail As String) As Boolean
On Error GoTo ErrHand:
Application.ScreenUpdating = False
Const olFolderCalendar As Byte = 9
Dim olApp As Object: Set olApp = CreateObject("Outlook.Application")
Dim olNS As Object: Set olNS = olApp.GetNamespace("MAPI")
Dim olFolder As Object
Dim olApt As Object
Dim objOwner As Object: Set objOwner = olNS.CreateRecipient(xEmail)
Dim FromDate As Date
Dim ToDate As Date
FindAttendance = False
objOwner.Resolve
If objOwner.Resolved Then
Set olFolder = olNS.GetSharedDefaultFolder(objOwner, olFolderCalendar)
End If
If olFolder.Items.Count = 0 Then Resume cleanExit
On Error Resume Next
For Each olApt In olFolder.Items
If olApt.Start = xDate Then
If (olApt.Subject = xSubject) Then
FindAttendance = True
Exit For
Else
End If
Else
End If
Next
On Error GoTo 0
Application.ScreenUpdating = True
cleanExit:
Application.ScreenUpdating = True
Exit Function
ErrHand:
Resume cleanExit
End Function

First of all, iterating over all items in the folder is not really a good idea. You need to use the Find/FindNext or Restrict methods of the Items class to get only items that correspond to your conditions.
Read more about that in the following articles:
How To: Retrieve Outlook calendar items using Find and FindNext methods
How To: Use Restrict method in Outlook to get calendar items
Second, don't use a straight comparison:
If (olApt.Subject = xSubject) Then
Instead, use the contains search criteria where a subject line may include a substring, not be equal. For example, the following query performs a phrase match query for keyword in the message subject:
filter = "#SQL=" & Chr(34) & "http://schemas.microsoft.com/mapi/proptag/0x0037001E" _
& Chr(34) & " ci_phrasematch " & "'keyword'"
Third, you may combine the following conditions into a single search string:
If olApt.Start = xDate Then
If (olApt.Subject = xSubject) Then

Never loop through all items in a folder, especially if it is an online (non-cached folder), that is what Items.Find/FindNext and Items.Restrict are for. Use a query like
#SQL="http://schemas.microsoft.com/mapi/proptag/0x0E1D001F" = 'TheValue'
Note that the search query above is on the PR_NORMALIZED_SUBJECT_W MAPI property; searches on the OOM property Subject are flaky in OOM.
You can add more conditions with OR or AND connectors. Also note that a check like If olApt.Start = xDate will most likely fail since Date values are floats and the condition will never be satisfied because of the round-off errors - always use a range (e.g. < start + 1 sec and > start - 1 sec)

Related

From excel vba, count only original emails

I am just beginning to get my feet under me in Excel VBA and now I need to (from Excel) count emails in Outlook over a specific timeframe. I hardcoded that timeframe in for now. That part seems to work - at least the count is correct.
The issue is that it's counting every single email, rather than just the originals. I need the count for just new emails. I have looked at .GetConversation and then read that conversationIDs change with each email, so the original has 44 characters. I thought that would be a good way to filter, but I do not understand what is in that property because it's not working.
Now I dont know if I'm barking up the wrong tree or if I'm just around the corner from getting this. It works fine until it tries to filter by the conversation ID.
Sub cntEmail()
'I WILL NEVER COUNT EMAILS AGAIN, dangit
Dim ns As Namespace: Set ns = GetNamespace(Type:="MAPI")
Dim fldr As Folder, fldrDone As Outlook.Folder
Dim inboxItems As Items, doneItems As Items, sFilter As String
Set fldr = ns.Folders("Call Center").Folders("Inbox")
Set fldrDone = ns.Folders("Call Center").Folders("DONE")
Set inboxItems = fldr.Items
Set doneItems = fldrDone.Items
sFilter = "[LastModificationTime] > '" & Format("1/13/2023 17:00", "ddddd h:mm AMPM") & "' AND [LastModificationTime] < '" & Format("1/20/2023 16:59", "ddddd h:mm AMPM") & "'"
Set inboxItems = inboxItems.Restrict(sFilter)
Set doneItems = doneItems.Restrict(sFilter)
Debug.Print "Total Inbox Count: " & inboxItems.Count
Debug.Print "Total Done Count: " & doneItems.Count
'Everything above this comment works
Set inboxItems = inboxItems.Restrict("[ConversationID] < 45")
Set doneItems = doneItems.Restrict("[ConversationID] < 45")
Debug.Print "Total Inbox Count: " & inboxItems.Count
Debug.Print "Total Done Count: " & doneItems.Count
Set fldr = Nothing
Set fldrDone = Nothing
Set ns = Nothing
End Sub
ConversationID
From what I understand ConversationID is a property that will have the same value for all the mailItems that belong to the same conversation (more here).
This means that if you reply to an email and the person replies to your reply, the second email you receive from them should have the same ConversationID.
I'm assuming that when you say that you want to count "original emails", you mean that you want to avoid counting the second email as it's part of the conversation initiated by the first (original) email.
So basically, you want to count how many unique values of ConversationID you have among your mailItems.
I haven't used .Restrict, so I'm not sure if you can use it for this purpose, but there are ways to get the total count of unique values for ConversationID by looping on the MailItems and counting the unique values.
Option 1: Using a Collection
One way to do it would be to use a collection. Since a collection can't contain two elements with the same key, we can use it to count the number of unique values.
For example:
Dim UniqueConversations As New Collection
Dim inboxItem As MailItem
For Each inboxItem In inboxItems
On Error Resume Next
'This line will return an error when the key already matches an item in the collection
'and the item won't be added to the collection.
UniqueConversations.Add 1, inboxItem.ConversationID
On Error GoTo 0
Next inboxItem
Debug.Print "Total Inbox Count: " & UniqueConversations.Count
Option 2: Using a Dictionary
The dictionary solution is a little more elegant as we don't need to use On error statements.
The reason why we don't get an error when we use a dictionary is that we'll just overwrite the stored value when the key already exists in the dictionary.
For example:
'Make sure to include Microsoft Scripting Runtime Library or use the drop-in replacement VBA-tools/VBA-Dictionary on Mac
Dim dict As Dictionary
Set dict = New Dictionary
Dim inboxItem As MailItem
For Each inboxItem In inboxItems
dict.Item(inboxItem.ConversationID) = 1
Next inboxItem
Debug.Print "Total Inbox Count: " & dict.Count
If you have a lot of emails, the dictionary approach is usually faster, but I haven't noticed a big difference for the small tests I've done.
You cannot create a restriction on property length like [ConversationID] < 45 (you can in Extended MAPI, but it is only available from C++ or Delphi). Try to create a restriction on PR_SUBJECT_PREFIX MAPI property being an empty string. On replies it is "RE" and "FW" on forwards.
#SQL="http://schemas.microsoft.com/mapi/proptag/0x003D001F" = ''
in your code:
Set inboxItems = inboxItems.Restrict("#SQL=""http://schemas.microsoft.com/mapi/proptag/0x003D001F"" = ''")
Set doneItems = doneItems.Restrict("#SQL=""http://schemas.microsoft.com/mapi/proptag/0x003D001F"" = ''")

Convert local time to UTC without calling Win APIs while taking into account DST

Turns out my organisation have blocked all Win API calls from MS Office code with their ASR rules and are unwilling to apply any exclusions so any old code I have no longer works. Anyone help with Excel VBA code that can convert local datetime into UTC?GMT while taking into account DST. I tried using the COM object, but wasn't sure what to do with DST offsets. Users are in multiple countries and I'm not sure exactly where since most people work remotely.
Was directed to this answer that uses Outlook which gives exactly what I wanted. Noticeably slower than the Win API calls on large scale repetitive calls, but any complaints can be directed to the company's IT for their security policy change.
https://stackoverflow.com/a/45510712/16578424
Option Explicit
'mTimeZones by Patrick Honorez --- www.idevlop.com
'with the precious help of Julian Hess https://stackoverflow.com/a/45510712/78522
'You can reuse but please let all the original comments including this one.
'This modules uses late binding and therefore should not require an explicit reference to Outlook,
'however Outlook must be properly installed and configured on the machine using this module
'Module works with Excel and Access
Private oOutl As Object 'keep Outlook reference active, to save time in recurring calls
Private oOutlTimeZones As Object 'keep Outlook reference active, to save time in recurring calls
' seems to drop the reference if use previous scheme of returning boolean
' returning the actual object is more correct in any case
Private Function GetOutlookTimeZones() As Object
If oOutl Is Nothing Or oOutlTimeZones Is Nothing Then
Debug.Print "~"
On Error Resume Next
Err.Clear
Set oOutl = GetObject(, "Outlook.Application")
If Err.Number Then
Err.Clear
Set oOutl = CreateObject("Outlook.Application")
End If
Set oOutlTimeZones = oOutl.TimeZones
End If
Set GetOutlookTimeZones = oOutlTimeZones
On Error GoTo 0
End Function
Function ConvertTime(DT As Date, Optional TZfrom As String = "Central Standard Time", _
Optional TZto As String = "W. Europe Standard Time") As Date
'convert datetime with hour from Source time zone to Target time zone
'valid Source & Target time zones can be found in your registry under: HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/Time Zones/
'this version using Outlook, properly handles Dailight Saving Times, including for past and future dates
'it includes a fix for the fact that ConvertTime seems to strip the seconds
'krammy85 2019-01-25 Edit: Outlook rounds minutes when it strips seconds, so modified code to strip seconds (without rounding) prior to running Outlook's ConvertTime.
Dim sourceTZ As Object
Dim destTZ As Object
Dim seconds As Single
Dim DT_SecondsStripped As Date
Dim oOutlTimeZones As Object: Set oOutlTimeZones = GetOutlookTimeZones()
If Not (oOutlTimeZones Is Nothing) Then
'fix for ConvertTime stripping the seconds
seconds = Second(DT) / 86400 'save the seconds as DateTime (86400 = 24*60*60)
DT_SecondsStripped = DT - seconds
Set sourceTZ = oOutlTimeZones.Item(TZfrom)
Set destTZ = oOutlTimeZones.Item(TZto)
ConvertTime = oOutlTimeZones.ConvertTime(DT_SecondsStripped, sourceTZ, destTZ) + seconds 'add the stripped seconds
End If
End Function
' returns number of minutes ahead of UTC (positive number) or behind
Function GetOffsetAt(DT As Date, TZfrom As String) As Long
Dim utc_DT As Date: utc_DT = ConvertTime(DT, TZfrom, "UTC")
GetOffsetAt = DateDiff("n", utc_DT, DT)
End Function
Sub test_ConvertTime()
Dim t As Date: t = #8/23/2017 6:15:05 AM#
Debug.Print t, ConvertTime(t), Format(t - ConvertTime(t), "h")
Debug.Print t, ConvertTime(t, "Central Standard Time", "W. Europe Standard Time"), Format(t - ConvertTime(t), "h")
End Sub
Sub test_DumpTZs()
Dim TZ As Object: For Each TZ In GetOutlookTimeZones()
Debug.Print "TZ:", TZ.Id, TZ.Name
Next TZ
End Sub
Last two Subs are not really necessary.
For example, I can now simply refer to the following to get the user's current timezone, regardless of daylight saving, and return the UTC converted value. This could be wrapped in a Format function to display accordingly.
ConvertTime(Now, oOutlTimeZones.CurrentTimeZone.id, "UTC")

Excel VBA - Outlook Email into Excel

I've done a fair bit of searching but everything I've come up with is doing the opposite of what I'm trying to do.
I have a whole bunch of automatically generated emails that I get, and I want to translate them down into excel. Everything works, except that it dumps it exclusively into one cell. I would like this to have multiple rows of the email come through as multiple lines in excel.
For example, email body is this. This will have a variable number of rows, so I can't really just use Mid functions.
Hello,
Job AAA completed successfully.
ThingA1 = good
ThingA2 = error code 5
This entire string shows up under cell A2 (which, is kinda what I told it to do...but I have no idea how to tell it to put it as multiple IDs). I want it to show up as different cells (covering cells A2:A6 in this instance).
Sub ParseAllEmails()
'loop through the outlook inbox, find stuff with errors, parse/paste it in
Dim OutApp As Outlook.Application, OLF As Outlook.MAPIFolder, OutMail As Outlook.MailItem
Dim myReport As Boolean, zeroErrors As Boolean
Dim parseSht As Worksheet
Dim i As Long
'establish connection
Set OutApp = CreateObject("Outlook.Application")
Set OLF = OutApp.GetNamespace("MAPI").GetDefaultFolder(olFolderInbox)
Set parseSht = ThisWorkbook.Sheets("parse")
'go through inbox looking for scheduler emails
For i = OLF.Items.Count To 1 Step -1
If TypeOf OLF.Items(i) Is MailItem Then
Set OutMail = OLF.Items(i)
myReport = (LCase(Left(OutMail.Subject, 3)) = "job")
zeroErrors = (InStr(1, LCase(OutMail.Subject), "errors=0") > 0)
If myReport And Not zeroErrors Then
parseSht.Range("A2:A500").Value = Trim(OutMail.Body)
Exit Sub
End If
End If
Next
End Sub
First of all, I'd suggest replacing the following part where the code iterates over all items in the Inbox folder:
'go through inbox looking for scheduler emails
For i = OLF.Items.Count To 1 Step -1
If TypeOf OLF.Items(i) Is MailItem Then
Set OutMail = OLF.Items(i)
myReport = (LCase(Left(OutMail.Subject, 3)) = "job")
zeroErrors = (InStr(1, LCase(OutMail.Subject), "errors=0") > 0)
If myReport And Not zeroErrors Then
Use the Find/FindNext or Restrict methods of the Items class which allow getting items that correspond to your conditions only. All you need is to iterate over the result collection and process such items after. Read more about these methods in the following articles:
How To: Use Find and FindNext methods to retrieve Outlook mail items from a folder (C#, VB.NET)
How To: Use Restrict method to retrieve Outlook mail items from a folder
To break the single message body string into separate lines you could use the Slit function available in VBA:
Dim strings() As String
strings = Split(mailItem.Body, vbNewLine)
So, you can detect the data which is required to be pasted and process these lines in the loop by adding each entry into a separate cell (if required).

Automating Text Extraction from Outlook to Excel

I'm a little out of my depth here, and definitely fumbling my way through trying to do this.
Scenario:
Emails arrive in a shared inbox every day for every new hire into the org. This is the full body of one of those emails:
The following are the new user details:
Full Name: Martha Washington
Employee ID: 123456
Department: Nursing Education and Research
Division: 17
RC: 730216
Job Title: Clin Nurse PRN Dept
Location: Medical Office Bldg West
Username: 12345678
I need to make/modify a script that will take only 3 lines out of this email body, and put them into columns in Excel. I need to get the Username value, the Job Title value, and the Location values and put them into separate columns. Then, the next email that arrives needs the same data extracted and put in a new row in Excel.
I want the Excel file to look something like this:
Username
JobTitle
Location
gwashing
President
Michigan
mwashing
Wife
New York
Any and all help is appreciated!
The Outlook object model provides the NewMailEx event of the Application class which is fired when a new message arrives in the Inbox and before client rule processing occurs. Use the Entry ID represented by the EntryIDCollection string to call the NameSpace.GetItemFromID method and process the item. This event fires once for every received item that is processed by Microsoft Outlook. The item can be one of several different item types, for example, MailItem, MeetingItem, or SharingItem. So, in the NewMailEx event you can get an instance of the incoming email where you could extract all the required information from the message body.
The Outlook object model provides three main ways for working with item bodies:
Body.
HTMLBody.
The Word editor. The WordEditor property of the Inspector class returns an instance of the Word Document which represents the message body.
See Chapter 17: Working with Item Bodies for more information.
I have something similar in my outlook application.
So this is Outlook VBA:
Sub Provtagning(msg As Outlook.MailItem)
Dim RE As Object
Dim objFolder As Outlook.MAPIFolder
Dim xExcelFile As String
Dim xExcelApp As Excel.Application
Dim wb As Excel.Workbook
xExcelFile = "Path to file"
' wait for file to be closed (if multiple mails arrive at the same time)
While IsWorkBookOpen(xExcelFile)
WasteTime (1)
Wend
DoEvents
Set xExcelApp = CreateObject("Excel.Application")
Set wb = xExcelApp.Workbooks.Open(xExcelFile)
Set RE = CreateObject("vbscript.regexp")
lrow = wb.Sheets("Sheet1").Cells(wb.Sheets("Sheet1").rows.Count, "A").End(xlUp).Row + 1
RE.Pattern = "Username:\s(\d+)"
Set allMatches = RE.Execute(msg.Body)
username = allMatches.Item(0).SubMatches.Item(0)
RE.Pattern = "Job Title:\s([a-zA-Z ]+)"
Set allMatches = RE.Execute(msg.Body)
title = allMatches.Item(0).SubMatches.Item(0)
RE.Pattern = "Location:\s([a-zA-Z ]+)"
Set allMatches = RE.Execute(msg.Body)
location = allMatches.Item(0).SubMatches.Item(0)
wb.Sheets("Sheet1").Range("A" & lrow).Value = username
wb.Sheets("Sheet1").Range("B" & lrow).Value = title
wb.Sheets("Sheet1").Range("C" & lrow).Value = location
wb.Save
wb.Close
End Sub
Sub WasteTime(Finish As Long)
Dim NowTick As Long
Dim EndTick As Long
EndTick = GetTickCount + (Finish * 1000)
Do
NowTick = GetTickCount
DoEvents
Loop Until NowTick >= EndTick
End Sub
Function IsWorkBookOpen(FileName As String)
Dim ff As Long, ErrNo As Long
On Error Resume Next
ff = FreeFile()
Open FileName For Input Lock Read As #ff
Close ff
ErrNo = Err
On Error GoTo 0
Select Case ErrNo
Case 0: IsWorkBookOpen = False
Case 70: IsWorkBookOpen = True
Case Else: Error ErrNo
End Select
End Function
You may need to adjust the regex patterns if there is something that is different.
Then just create a rule in Outlook to run this script on every email that is from SomeEmail or whatever that is the trigger.

outlook "To Do" items into Excel using VBA

First off, I'm new to VBA, with about 20 hours of training.
I'm trying to export items from Outlook 2010 to Excel 2010. I want to grab all the unfinished "To Do" items from Outlook and throw them into Excel with one item per row, and columns for item parameters (like Subject, Due Date, attachments, etc.).
Here's the first pass that actually does what I explained, and imports only tasks (tasks are a subset of all to do items, from what I understand) and their Subject/Due Date:
Function GetOutlookApp() As Object
On Error Resume Next
Set GetOutlookApp = GetObject(, "Outlook.Application")
If Err.Number <> 0 Then
Set GetOutlookApp = CreateObject("Outlook.Application")
End If
On Error GoTo 0
End Function
Sub getOlTasks()
Dim olApp As Object ' Outlook.Application
Dim olnameSpace As Object ' Outlook.Namespace
Dim taskFolder As Object ' Outlook.MAPIFolder
Dim tasks As Object ' Outlook.Items
Dim tsk As Object
Set olApp = GetOutlookApp
Set olnameSpace = olApp.GetNamespace("MAPI")
Set taskFolder = olnameSpace.GetDefaultFolder(13) 'olFolderTasks is folder# 13, apparently
Set tasks = taskFolder.Items
For x = 1 To tasks.Count
Set tsk = tasks.Item(x)
Sheet1.Cells(1, 1).Activate
Do While IsEmpty(ActiveCell) = False
Selection.Offset(1, 0).Activate
Loop
'Fill in Data
If Not tsk.Complete Then
ActiveCell.Value = tsk.Subject
Selection.Offset(0, 1).Activate
ActiveCell.Value = tsk.DueDate
Selection.Offset(1, -1).Activate
End If
Next x
End Sub
I tried to do this with only "tasks" items, everything was going smoothly until I realized that tasks can't have attachments. When I have an email w/attachment that I turn into a task, I lose the attachment. Apparently what I need to do is import all "To Do items", rather than just tasks.
So My questions are:
1) What folder number is olFolderToDo? I have seen people run code like:
Set taskFolder = olnameSpace.GetDefaultFolder(olFolderTasks) 'rather than GetDefaultFolder(13)
which would lead me to believe I should be able to just use olFolderToDo, but when I try to use the name of the folder in my first example rather than the number, I get an invalid argument runtime error. If anyone knows why, I'd be interested to know.
2) How would I go about importing an attachment from an email to a specific cell in excel?
See OlDefaultFolders Enumeration (Outlook)
Name Value Description
olFolderToDo 28 The To Do folder.

Resources