From excel vba, count only original emails - excel

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"" = ''")

Related

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).

Narrowing down / speeding up search in extracted outlook shcedule

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)

To check if you have replied to client email via vba

Greetings for the day!
I have written a small VBA code to check if my team has responded to the client's email or not. on daily basis we get approx 500+ emails from the client, to track the same I have written the below code to check what all emails are being looked upon.
Dim O As Outlook.Application
Dim R As Long
Sub project2()
Set O = New Outlook.Application
Dim Omail As Outlook.MailItem
Set Omail = O.CreateItem(olMailItem)
Dim ONS As Outlook.Namespace
Set ONS = O.GetNamespace("MAPI")
Dim FOL As Outlook.Folder
Set FOL = ONS.GetDefaultFolder(olFolderInbox).Folders("MD-GPS")
R = 2
For Each Omail In FOL.Items
Cells(R, 1) = Omail.Subject
Cells(R, 2) = Omail.SenderEmailAddress
Call REPLY_STATUS(Omail.Subject, Omail.SenderEmailAddress)
R = R + 1
On Error Resume Next
Next Omail
End Sub
Sub REPLY_STATUS(MailSubject As String, MailSender As String)
Dim SentEmail As Outlook.MailItem
Set SentEmail = O.CreateItem(olMailItem)
Dim ONS2 As Outlook.Namespace
Set ONS2 = O.GetNamespace("MAPI")
Dim FOL2 As Outlook.Folder
Set FOL2 = ONS2.GetDefaultFolder(olFolderSentMail)
Dim check As String
check = "RE: " & MailSubject
For Each SentEmail In FOL2.Items
If check = SentEmail.Subject And MailSender = SentEmail.Recipients(1).Address Then
Cells(R, 3) = "Yes"
Exit For
Else
End If
On Error Resume Next
Next SentEmail
End Sub
But the ending is not that great as it looks, the code is working but
in most cases, the code captures something else rather than capturing the sender's email address in an excel sheet.
As we daily receive 500+ emails, the code becomes too slow as it checks the entire folder from the scratch, is there a possibility I can add a start date that I can mention in the excel sheet and the code will start from that date only.
Not sure why it is also not filling column 3 i.e. if replied however my team has actually replied to those emails.
it is not picking up the latest emails from the defined sub-folder ("MD-GPS"), why is that happening?
Any help on this would be greatly appreciated.
Note: To handle stmp exchange account error, I tried using the following MailItem.Sender.GetExchangeUser.PrimarySmtpAddress but the only drawback is if I change the sub-folder to something else, it doesn't work.
Firstly, you do not need to create SentEmail - get rid of the
Set SentEmail = O.CreateItem(olMailItem)
line.
Secondly, never loop through all items in a folder - use Items.Find/FindNext or Items.Restrict.
Thirdly, you are seeing an EX type address (as opposed to SMTP). Check MailItem.Sender.Type property - if it is "SMTP", use MailItem.Sender.Address. Otherwise (in case of "EX") use MailItem.Sender.GetExchangeUser().PrimarySmtpAddress.
That being said, you can check if anybody replied to the original message at all - check if PR_LAST_VERB_EXECUTED MAPI property (DASL name http://schemas.microsoft.com/mapi/proptag/0x10810003) is present - 103 is EXCHIVERB_REPLYTOALLand 102 is EXCHIVERB_REPLYTOSENDER. If the property is not set at all, there is no reason to search.
To search for a matching subject, use a query like
"[Subject] = ' & MailSubject & "'"
Note that Outlook Object Model will not let you search on the message recipients or attachments. If using Redemption (I am its author) is an option, you can use something like the following. You can specify Recipients as one of the search fields, and Redemption will create a restriction on recipient name / address / SMTP address
set session = CreateObject("Redemption.RDOSession")
session.MAPIOBJECT = O.Session.MAPIOBJECT
set SentEmail = FOL2.Items.Find("""http://schemas.microsoft.com/mapi/proptag/0x0E1D001F"" = '" & MailSubject & "' AND Recipients = '" & MailSender & "'")
Note that most MAPI stores won't search on PR_SUBJECT, only on PR_NORMALIZED_SUBJECT (which is the subject without the RE: or FW: prefix) - which is what the snippet above is using.

Looping through Arrays with VBA, to Move outlook emails from one folder to another?

I want to move emails of invoices from a main folder to a different folder.
I extracted the subject of the emails with VBA from outlook in the first module, they are in column 3. Then I manually write out the folder I would like the emails to move to, in column 8. (The names of the folder is a subfolder)
Column 3 is the subject of the email which I extracted, I used the restrict method for outlook to return the email with the specific tittle
Column 8 is the folder I would like the email to move too.
Example is like below
The code has to place email in the main folder with subject'A' to Folder '1'
Column 3 columnn 8
A 1
B 2
C 2
D 1
E 1
The reason I use arrays is because, every time I make an extract, the list changes, hence it is dynamic. Therefore, I used LBound and UBound to include the whole list of invoices.
I have declared all variables here in the first module as 'public'. Only left the relevant ones here to the code
Sub MovingEmails_Invoices()
'Declare your Variables
Dim i As Object
Dim myitems As Object
Dim subfolder As Outlook.Folder
'Set Outlook Inbox Reference
Set OP = New Outlook.Application
Set NS = OP.GetNamespace("MAPI")
'To loop through subfolder and its folders
Set rootfol = NS.Folders("SYNTHES-JNJCZ-GBS.DE.AT.CH#ITS.JNJ.com")
Set Folder = rootfol.Folders("Austria")
'The list for invoice numbers and folders is dynamic
'Each subject being searched is different
Dim Listmails() As Variant
Dim Rowcount As Long
Dim Mailsubject As Variant
Dim FolderName As Variant
Dim MS As String
Dim myrestrictitem As Outlook.items
'Establish the array based on the mailbox extract
Sheets("files").Activate
Listmails = Range("A2").CurrentRegion
'Ititerate through the array which is dynamic (One-dimensional)
For Rowcount = LBound(Listmails) To UBound(Listmails)
'3rd row for email subject 'used DASL Filter
Mailsubject = Application.WorksheetFunction.Index(Listmails, Rowcount, 3)
MS = "urn:schemas:mailheader:subject LIKE \'%" & Mailsubject & "%\'"
'Find the email based on the array for email subject
Set myitems = Folder.items
Set myrestrictitem = myitems.Restrict(MS)
For each i in myrestrictitem
If i.class = olmail then
'8th row for folder name
FolderName = Application.WorksheetFunction.Index(Listmails, Rowcount,8)
Set subfolder = rootfol.Folders(FolderName) ' i have an error here
'If email found then mark it as read
i.UnRead = False
'Move it to the subfolder based on the array for folder name
i.Move subfolder
Next Rowcount
End Sub
Now, I used the example I got from Microsoft Office Center to construct the restrict part, the last example on this page: https://learn.microsoft.com/en-us/office/vba/api/outlook.items.restrict
when I try to do the same way, it doesn't work for my code.
The error message comes from;
Set myrestrictitem = myitems.Restrict(MS)
and
?
Set subfolder = rootfol.Folders(FolderName)
The error message is the condition is not correct. Also it could be because I am doing the loop incorrectly.
Could there be another way of doing this, without arrays maybe? do i need IF condition?
You condition must include the #SQL= prefix. It is also a good idea to double quote the DASL property names:
#SQL="urn:schemas:mailheader:subject" LIKE '%test%'
You also should not use "for each" when you are changing the collection (by calling Move). Use a down loop:
for i = myrestrictitem.Count to 1 step -1
set item = myrestrictitem.Item(i)
..
item.Move subfolder

Find First Blank Cell Matching Specific Criteria with Matching Values

Yeow... That was a mouthful.
I'm setting up a log to track inventory in the office. For some items we have multiple copies of the same tool. I've gotten the VBA to track the owners with userforms and vba, but the multiple-identical-names, first-occurance, blank cell trick is proving to be too much for me.
So what you have is this:
Item Serial Number Owner
Item A 999999999
Item A 999999991
Item A 999999992
Item B 22221
Item B 22222
Item B 22223
Item C hhhg77
Item C hhhg78
Item C hhhg79
I need the code to search for the ITEM name, gathered from ComboBox1 on the Userform, and find the first occurance of the ITEM WITHOUT an owner, (so the corresponding "OWNER" cell should be blank, and put the OWNER, gathered from ComboBox2 on the userform, in that spot.
I've been fooling around with Index & Match and VLookup and countless searches for "Finding First Row" but I've come up empty.
The ranges might be changed, so I'm hesitant to be so specific as to say search between A2:A4, so a search would be best, I think.
What I have so far is this....and it's weak, I apologize.
Public Sub FindBlankOwner()
Dim MultiItem As Range
Dim MultiOwner As Range
Dim ITEM As String
Dim OWNER As String
Dim MultiSerial As Range
Dim NO As Range
ITEM = ComboBox1.Value
STATUS = Application.WorksheetFunction.VLookup(ITEM, LUP, 6, False)
OWNER = ComboBox2.Value
Set ws = Worksheets("Owners")
Set MultiItem = Worksheets("Owners").Range("A1:A28")
Set MultiOwner = Worksheets("Owners").Range("C1:C28")
Set MultiSerial = Worksheets("Owners").Range("B1:B28")
Fillin = Evaluate("INDEX(MultiOwner, MATCH(ITEM, &
' Set FILLIN = Application.WorksheetFunction.Match(ITEM, (MultiItem), 0) And (Application.WorksheetFunction.Match(" ", (MultiOwner), 0))
' NO.Value = OWNER
'Set FILLIN = MultiItem.Find(What:=ITEM) And MultiOwner.Find(What:="")
End Sub
Search for Available Items Based on Empty Owner
I've written some raw code for your review. You were on the right track, but with the methods below we avoid using worksheet functions and instead use VBA methods.
First thing we want to do is find the first occurrence of the user-defined ITEM
After that we check if it has an OWNER or not. If it does, find the next ITEM. If it doesn't, assign the user-defined OWNER to the user-defined ITEM.
I've included some message boxes for clarity and convenience of the end-user.
The Code
Sub FindBlankOwner()
Dim ws As Worksheet: Set ws = ThisWorkbook.Sheets("Owners")
Dim ITEM As String: ITEM = ComboBox1.Value
Dim OWNER As String: OWNER = ComboBox2.Value
Dim BlankOwnerFound As Boolean: BlankOwnerFound = False
Dim firstResult As String
Dim ItemRange As Range: Set ItemRange = ws.Range("A:A").Find(What:=ITEM, LookIn:=xlValues, LookAt:=xlWhole)
If Not ItemRange Is Nothing Then
firstResult = ItemRange.Address
Do
If Trim(ItemRange.Offset(0, 2).Value) = "" Then
ItemRange.Offset(0, 2).Value = OWNER
BlankOwnerFound = True
Else
Set ItemRange = ws.Range("A:A").FindNext(ItemRange)
End If
Loop While Not ItemRange Is Nothing And ItemRange.Address <> firstResult And BlankOwnerFound = False
Else: MsgBox "No results for " & ITEM & " found.", vbCritical, "Cannot Find " & ITEM
End If
If BlankOwnerFound = True Then
MsgBox OWNER & " has checked out " & ITEM & " with Serial Number " & ItemRange.Offset(0, 1).Value & ".", _
vbOKOnly, ITEM & " Check-Out Successful"
Else: MsgBox "No available " & ITEM & " in inventory.", vbCritical, "All " & ITEM & " are checked out."
End If
End Sub
NOTE: I haven't tested this code aside from reading it to myself to ensure it makes sense. There may be a couple errors (hopefully not) and if you can't solve them, please don't hesitate to let me know so we can work on it together. Let me know if this works for you :)

Resources