Looping through large set of mailitems freezes Outlook - vba

I am trying to loop through the mailItems of different subfolders looking for a comment (with PropertyAccessor) in different stores.
My code works when given 1-3 stores and around 2000 mailItems, however as the number increases it crashes Outlook.
I have implemented:
Date filtering
MailItem release
I am trying parallely the Application.AdvancedSearch method, however not managing yet.
Sub FindEmaibyComment()
Dim Fldr As Outlook.folder
Dim Str As Outlook.Store
Dim Strs As Outlook.Stores
Dim Pfldr As Outlook.folder
Dim oRoot As Outlook.folder
Dim clearingFolder As Outlook.folder
Dim mail As MailItem
Dim TaskID As String
Set Strs = Application.Session.Stores
TaskID = InputBox("Enter the MailID you want to look for." & vbNewLine & "(For example MAIL_20200525_1502769)", "Message input", "")
Set RegEx = CreateObject("vbscript.regexp")
With RegEx
.MultiLine = False
.Global = True
.IgnoreCase = True
.Pattern = "MAIL_" & "[0-9]{8}" & "_" & "[0-9]{6,100}" ' <-- Regex match for input string, example string: MAIL_20200513_1434402
End With
Set Match = RegEx.Execute(TaskID)
If Match.Count = 1 Then
'Select the stores that contain ICE, and loop through them
For Each Str In Strs
If InStr(Str.DisplayName, "Mailbox1") > 0 Then
On Error Resume Next '--> In case no permission for the store is given, go to the next store
Set oRoot = Str.GetRootFolder
Set clearingFolder = LoopFolders(oRoot, TaskID)
End If
Next Str
If MailFound = False Then
MsgBox ("Sorry, I could not find the Email")
End If
Else
MsgBox ("Please insert the correct ID with a format as follows: MAIL_12345678_1234567")
End If
End Sub
Function LoopFolders(ByVal oFolder As Outlook.folder, TaskID As String) As Outlook.folder
Dim folders As Outlook.folders
Dim Subfolders As Outlook.folders
Dim folder As Outlook.folder
Dim SubFolder As Outlook.folder
Dim foldercount As Integer
Dim clearingFolder As Outlook.folder
On Error Resume Next
Set folders = oFolder.folders
foldercount = folders.Count
'Count folders below oFolder. This is the store level
If foldercount Then
For Each folder In folders
'Count folders below Folder. This is 1. folder level "AE01"
If folder.folders.Count > 0 Then
Set Subfolders = folder.folders
For Each SubFolder In Subfolders
'Subfolders below Folder. This is 2. folder level "Clearing", "Destination"
If InStr(SubFolder, "Destination") > 0 Then
Set LoopFolders = SubFolder
FindID TaskID, SubFolder
End If
Next SubFolder
End If
Next folder
End If
End Function
Function FindID(TaskID As String, folderClearing As Outlook.folder)
Dim PropName, Comment, MessageID As String
Dim oMail As MailItem
Dim oPA As Outlook.PropertyAccessor
Dim olFolder As Outlook.MAPIFolder
Dim olNamespace As Outlook.NameSpace
Dim inputDate, inputDay, inputYear, inputMonth, sFilter, inputDateConverted, startDay, endDay As String
inputDate = Right(Left(TaskID, 13), 8) 'Example: 20200610
inputYear = Left(inputDate, 4)
inputDay = Right(inputDate, 2)
inputMonth = Right(Left(inputDate, 6), 2)
If Left(inputDay, 1) = "0" Then
inputDay = Right(inputDay, 1)
End If
If Left(inputMonth, 1) = "0" Then
inputMonth = Right(inputMonth, 1)
End If
inputDateConverted = inputMonth & "/" & inputDay & "/" & inputYear
startDay = Format(CDate(inputDateConverted & " 00:00 AM "), "\'m/d/yyyy hh:mm AM/PM\'")
endDay = Format(CDate(inputDateConverted & " 12:00 PM"), "\'m/d/yyyy hh:mm AM/PM\'")
Set myItems = folderClearing.Items
sFilter = startDay & " > [ReceivedTime] And" & endDay & " < [ReceivedTime]"
Set myRestrictedItems = myItems.Restrict(sFilter)
For Each oMail In myRestrictedItems
'PR_TRANSPORT_COMMENTS
PropName = "http://schemas.microsoft.com/mapi/proptag/0x3004001F"
Set oPA = oMail.PropertyAccessor
Comment = oPA.GetProperty(PropName)
If InStr(1, Comment, TaskID, vbTextCompare) > 0 Then
MailFound = True
MsgBox ("Mail was found in Company Code " & folderClearing.Parent & ", let me open it for you")
oMail.Display
End
End If
Set oMail = Nothing
Next oMail
End Function

Restrict and Find/FindNext methods are run on the main thread which means they block the UI and the overall user experience with Outlook. Moreover, iterating over all folder and subfolders is not really a good idea for searching items. That is for AdvancedSearch was introduced!
The key benefits of using the AdvancedSearch method in Outlook are:
The search is performed in another thread. You don’t need to run another thread manually since the AdvancedSearch method runs it automatically in the background.
Possibility to search for any item types: mail, appointment, calendar, notes etc. in any location, i.e. beyond the scope of a certain folder. The Restrict and Find/FindNext methods can be applied to a particular Items collection (see the Items property of the Folder class in Outlook).
Full support for DASL queries (custom properties can be used for searching too). You can read more about this in the Filtering article in MSDN. To improve the search performance, Instant Search keywords can be used if Instant Search is enabled for the store (see the IsInstantSearchEnabled property of the Store class).
You can stop the search process at any moment using the Stop method of the Search class.
Read more about the AdvancedSearch method in the Advanced search in Outlook programmatically: C#, VB.NET article.
Using the Scope parameter, you can specify one or more folders in the same store, but you may not specify multiple folders in multiple stores. To specify multiple folders in the same store for the Scope parameter, use a comma character between each folder path and enclose each folder path in single quotes. For default folders such as Inbox or Sent Items, you can use the simple folder name instead of the full folder path.
You can run multiple searches simultaneously by calling the AdvancedSearch method in successive lines of code. However, you should be aware that programmatically creating a large number of search folders can result in significant simultaneous search activity that would affect the performance of Outlook, especially if Outlook conducts the search in online Exchange mode.
Public m_SearchComplete As Boolean
Private Sub Application_AdvancedSearchComplete(ByVal SearchObject As Search)
If SearchObject.Tag = "MySearch" Then
m_SearchComplete = True
End If
End Sub
Sub TestSearchForMultipleFolders()
Dim Scope As String
Dim Filter As String
Dim MySearch As Outlook.Search
Dim MyTable As Outlook.Table
Dim nextRow As Outlook.Row
m_SearchComplete = False
'Establish scope for multiple folders
Scope = "'" & Application.Session.GetDefaultFolder( _
olFolderInbox).FolderPath _
& "','" & Application.Session.GetDefaultFolder( _
olFolderSentMail).FolderPath & "'"
'Establish filter
If Application.Session.DefaultStore.IsInstantSearchEnabled Then
Filter = Chr(34) & "urn:schemas:httpmail:subject" _
& Chr(34) & " ci_phrasematch 'Office'"
Else
Filter = Chr(34) & "urn:schemas:httpmail:subject" _
& Chr(34) & " like '%Office%'"
End If
Set MySearch = Application.AdvancedSearch( _
Scope, Filter, True, "MySearch")
While m_SearchComplete <> True
DoEvents
Wend
Set MyTable = MySearch.GetTable
Do Until MyTable.EndOfTable
Set nextRow = MyTable.GetNextRow()
Debug.Print nextRow("Subject")
Loop
End Sub

A common cause of mysterious failure is hiding errors with On Error Resume Next.
For Each Str In Strs
Debug.Print "Str.DisplayName: " & Str.DisplayName
If InStr(Str.DisplayName, "Mailbox1") > 0 Then
Set oRoot = Nothing ' else bypssing expected error keeps previous value in oRoot
' bypass expected error
On Error Resume Next '--> In case no permission for the store is given, go to the next store
Set oRoot = Str.GetRootFolder
' remove error bypass
' to return to normal error handling
' to deal with unexpected errors
On Error GoTo 0 ' now you can see errors and can debug your code
' Handle the bypassed error
If Not oRoot Is Nothing Then
Set clearingFolder = LoopFolders(oRoot, TaskID)
End If
End If
Next Str

Related

How to apply a DASL filter in AdvancedSearch?

I adapted code so I can reply to the latest email.
I loop through a range of cells in my spreadsheet to get a string to find the email in my inbox or sent items.
The code sometimes finds and opens the email thread and sometimes it won't.
Is the syntax of my filter correct?
searchString = "urn:schemas:httpmail:textdescription ci_phrasematch" & supNumber
I commented these lines otherwise it won't stop looping:
While searchComplete = False
' DoEvents
Wend
The event handler OutlookApp_AdvancedSearchComplete never fires
The following code is saved in a class module:
Option Explicit
' Credits: Based on this answer: https://stackoverflow.com/questions/31909315/advanced-search-complete-event-not-firing-in-vba
' Event handler for outlook
Dim WithEvents OutlookApp As Outlook.Application
Dim outlookSearch As Outlook.Search
Dim outlookResults As Outlook.Results
Dim searchComplete As Boolean
Private Declare Function GetTickCount Lib "Kernel32" () As Long
Sub Minuterie(Milliseconde As Long)
Dim Arret As Long
Arret = GetTickCount() + Milliseconde
Do While GetTickCount() < Arret
DoEvents
Loop
End Sub
' Handler for Advanced search complete
Private Sub OutlookApp_AdvancedSearchComplete(ByVal SearchObject As Search)
MsgBox "The AdvancedSearchComplete Event fired."
searchComplete = True
End Sub
Sub SearchAndReply(program_number As Range, searchFolderName As String, searchSubFolders As Boolean)
' Declare objects variables
Dim customMailItem As Outlook.MailItem
Dim searchString As String
Dim resultItem As Integer
Dim supNumber As String
Dim compName As String
Dim strFilter As String
Dim OutlookApp As Outlook.Application
Dim strTag As String
Dim answer As VbMsgBoxResult
' Variable defined at the class level
'Dim outlookSearch As Outlook.Search
Set OutlookApp = New Outlook.Application
strTag = "BodySearch"
' Variable defined at the class level (modified by outlookApp_AdvancedSearchComplete when search is completed)
searchComplete = False
supNumber = "'" & program_number.Value & "'"
searchString = "urn:schemas:httpmail:textdescription ci_phrasematch" & supNumber
' Perform advanced search
Set outlookSearch = OutlookApp.AdvancedSearch(searchFolderName, searchString, searchSubFolders)
Minuterie 2000
' Wait until search is complete based on outlookApp_AdvancedSearchComplete event
' While searchComplete = False
' DoEvents
' Wend
' Get the results
Set outlookResults = outlookSearch.Results
If outlookResults.Count = 0 Then
program_number.Interior.Color = vbRed
Exit Sub
End If
' Sort descending so you get the latest
outlookResults.Sort "[SentOn]", True
' Reply only to the latest one
resultItem = 1
' Some properties you can check from the email item for debugging purposes
On Error Resume Next
Debug.Print outlookResults.Item(resultItem).SentOn, outlookResults.Item(resultItem).ReceivedTime, outlookResults.Item(resultItem).SenderName, outlookResults.Item(resultItem).subject
On Error GoTo 0
Set customMailItem = outlookResults.Item(resultItem).ReplyAll
' At least one reply setting is required in order to replyall to fire
'customMailItem.Body = "Just a reply text " & customMailItem.Body
customMailItem.HTMLBody = "<p> Thank you <p>" & customMailItem.HTMLBody
customMailItem.Display
program_number.Interior.Color = vbYellow
End Sub
I saved the following code in regular module in Excel:
Public Sub ProcessEmails()
Dim testOutlook As Object
Dim oOutlook As clsOutlook
Dim searchRange As Range
Dim subjectCell As Range
Dim OGDD_Programs As Range
Dim searchFolderName As String
Dim answer As VbMsgBoxResult
Dim Sup_ENg_Number As Range
' Start outlook if it isn't opened (credits: https://stackoverflow.com/questions/33328314/how-to-open-outlook-with-vba)
On Error Resume Next
Set testOutlook = GetObject(, "Outlook.Application")
On Error GoTo 0
If testOutlook Is Nothing Then
Shell ("OUTLOOK")
End If
' Initialize Outlook class
Set oOutlook = New clsOutlook
' Get the outlook inbox and sent items folders path (check the scope specification here: https://learn.microsoft.com/en-us/office/vba/api/outlook.application.advancedsearch)
searchFolderName = "'" & Outlook.Session.GetDefaultFolder(olFolderInbox).FolderPath & "','" & Outlook.Session.GetDefaultFolder(olFolderSentMail).FolderPath & "'"
'
' ' Loop through excel cells with subjects
' Set searchRange = ThisWorkbook.Worksheets("Sheet1").Range("A2:A4")
'set a reference to cells we are going to loop through
Set OGDD_Programs = ActiveSheet.Range("A2", "A" & Cells(Rows.Count, 1).End(xlUp).Row).SpecialCells(xlCellTypeVisible)
For Each Sup_ENg_Number In OGDD_Programs
If (Sup_ENg_Number.Interior.Color = vbYellow Or Sup_ENg_Number.Interior.Color = vbRed) Then
Else
' Only to cells with actual subjects
If Sup_ENg_Number.Value <> vbNullString Then
Call oOutlook.SearchAndReply(Sup_ENg_Number, searchFolderName, True)
answer = MsgBox("Do you want to exit subRoutine ?", vbYesNo)
If answer = vbYes Then
Exit Sub
End If
End If
End If
Next Sup_ENg_Number
MsgBox "Search and reply completed"
' Clean object
Set testOutlook = Nothing
End Sub
'Then add a class module and name it: clsOutlook
Why do you need AdvancedSearch (which is asynchronous)? Use MAPIFolder.Items.Find. (where MAPIFolder is the folder you need to search, such as the Inbox retrieved using Application.Session.GetDEfaultFolder(olFolderInbox)).
The code should first check if Instant Search is enabled in the default store to determine whether to use the ci_phrasematch keyword for an exact phrase match of "keyword" in the item body, or the like keyword to match any occurrence of "keyword" as an exact string or a substring in the item body. For example:
Dim filter As String
If (Application.Session.DefaultStore.IsInstantSearchEnabled) Then
filter = "#SQL=" & Chr(34) _
& "urn:schemas:httpmail:textdescription" & Chr(34) _
& " ci_phrasematch 'office'"
Else
filter = "#SQL=" & Chr(34) _
& "urn:schemas:httpmail:textdescription" & Chr(34) _
& " like '%office%'"
End If
The key benefits of using the AdvancedSearch method in Outlook are:
The search is performed in another thread. You don’t need to run another thread manually since the AdvancedSearch method runs it automatically in the background.
Possibility to search for any item types: mail, appointment, calendar, notes etc. in any location, i.e. beyond the scope of a certain folder. The Restrict and Find/FindNext methods can be applied to a particular Items collection (see the Items property of the Folder class in Outlook).
Full support for DASL queries (custom properties can be used for searching too). You can read more about this in the Filtering article in MSDN. To improve the search performance, Instant Search keywords can be used if Instant Search is enabled for the store (see the IsInstantSearchEnabled property of the Store class).
You can stop the search process at any moment using the Stop method of the Search class.
Read more about that in the Advanced search in Outlook programmatically: C#, VB.NET article.
Re: The event handler OutlookApp_AdvancedSearchComplete never fires.
I have to work around this event.
Option Explicit
Private Sub Get_LastMail_AdvSearch_URN_Subject()
Dim strSearch As String
Dim strFilter As String
Dim strScope As String
Dim objSearch As Search
Dim fldrNm As String
Dim rsts As results
Dim rstObj As Object
Debug.Print
strScope = "'Inbox', 'Sent Items', 'Deleted Items'"
'strScope = "'Inbox', 'Sent Items'"
Debug.Print "strScope............: " & strScope
strSearch = "test"
fldrNm = "Subject: " & strSearch
Debug.Print fldrNm
strFilter = "urn:schemas:httpmail:subject LIKE '%" & strSearch & "%'"
Debug.Print strFilter
Set objSearch = AdvancedSearch(scope:=strScope, Filter:=strFilter, SearchSubFolders:=True, Tag:="SearchFolder")
' The Application.AdvancedSearchComplete event is problem
' https://learn.microsoft.com/en-us/office/vba/api/outlook.application.advancedsearchcomplete
' https://stackoverflow.com/questions/31909315/advanced-search-complete-event-not-firing-in-vba
' Saving may be enough. This would be ideal but not on my machine.
' You may have to delete a previously generated search folder before subsequent runs
objSearch.Save fldrNm
DoEvents
Debug.Print fldrNm & " saved."
Set rsts = objSearch.results
Debug.Print " rsts.Count: " & rsts.Count
If rsts.Count = 0 Then
Debug.Print "Delay initiated."
' Delay to allow the search to complete
Dim waitTime As Long
Dim delay As Date
' Will surely be too little at the most inopportune time
waitTime = 1 ' in seconds - adjust as needed
Debug.Print vbCr & "Wait start: " & Now
delay = DateAdd("s", waitTime, Now)
Debug.Print "Wait until: " & delay
Do Until Now > delay
DoEvents
Loop
End If
Set rsts = objSearch.results
Debug.Print " rsts.Count: " & rsts.Count
If rsts.Count > 0 Then
rsts.Sort "[ReceivedTime]", True
Set rstObj = rsts(1)
Debug.Print rstObj.subject
Else
Debug.Print "no items found."
End If
End Sub

Server based rule to collate 500+ adresses into ~150 inbox folders

I have a Company Project where ~500 clients send Emails to the my project inbox. Those clients correspond to ~150 offices (I have an Excel-List of the email addresses & according offices).
Each office shall have one Outlook folder, so I can quickly check upon the past correspondence with a specific office.
The Project inbox is looked after and used by several co-workers, hence server- and not client based rules.
How do I set this up?
My simple idea in form of a pseudo code:
for each arriving email
if (from-adress is in "email & office-List")
move that email to outlook folder "according office name"
end if
end for
and the same for outgoing emails:
for each sent email
if (to-adress is in "email & office-List")
move that email to outlook folder "according office name"
end if
end for
Thanks for suggestions!
...and besides, can outlook folders be created programmatically from a list of names?
My solution is a skript i run daily on a manual basis since my employer doesnt allow scripts on arriving messages.
the logic in short is:
fetch list of emails & their corresponding offices (both string lists)
set up folder variables
loop through messages, and move them eventually
the code looks like
Option Compare Text ' makes string comparisons case insensitive
Sub sortEmails()
'sorts the emails into folders
Dim msg As Outlook.MailItem
Dim itm As Object
Dim adress As String
Dim pa As Outlook.PropertyAccessor
Const PR_SMTP_ADDRESS As String = _
"http://schemas.microsoft.com/mapi/proptag/0x39FE001E"
'1) fetch emails
GetEMailsFolders locIDs, emails, n
'1.5) fetch folder objects
'Create an instance of Outlook & inbox reference
Dim Inbox As Outlook.MAPIFolder
Dim outbox As Outlook.MAPIFolder
Set outlookApp = New Outlook.Application
Set NS = outlookApp.GetNamespace("MAPI")
Set objOwner = NS.CreateRecipient("email#host.com")
objOwner.Resolve
'Set inbox = NS.GetSharedDefaultFolder(objOwner, olFolderInbox)
Set Inbox = NS.Folders("email#host.com").Folders("Inbox")
Set outbox = NS.Folders("email#host.com").Folders("Sent Items")
Dim basefolder As Outlook.MAPIFolder
Dim bfName As String
bfName = "Offices" 'name of the folder for the offices
Set basefolder = MkDirConditional(Inbox.Folders("Project folder"), bfName)
'2)loop through inbox & outbox emails
Dim destination As Outlook.MAPIFolder
Dim fold(1 To 2) As Outlook.MAPIFolder
Set fold(1) = Inbox
Set fold(2) = outbox
Dim LocID As String
For Each fol In fold 'loop through inbox & outbox
Debug.Print fol
'reverse fo loop because otherwise moved messages modify indices of following messages
For i = fol.Items.Count To 1 Step -1 'Each itm In fol.Items
Set itm = fol.Items(i)
If TypeName(itm) = "MailItem" Then ' others are AppointmentItem, MeetingItem, or TaskItem
Set msg = itm
'Debug.Print " " & msg.Subject
If fol = Inbox Then
' there are two formats of email adrersses.
If msg.SenderEmailType = "EX" Then 'check two kinds of email adress formats
adress = msg.Sender.GetExchangeUser().PrimarySmtpAddress
ElseIf msg.SenderEmailType = "SMTP" Then 'SMTP case
adress = msg.SenderEmailAddress
Else
Debug.Print " neither EX nor SMTP" & msg.Subject;
End If
pos = Findstring(adress, emails) ' position in the email / standort list
ElseIf fol = outbox Then
For Each rec In msg.Recipients
Set pa = rec.PropertyAccessor
adress = pa.GetProperty(PR_SMTP_ADDRESS)
pos = Findstring(adress, emails)
If pos > 0 Then
Exit For
End If
Next rec
End If
'4.5) if folder doesnt exist, create it
'5) move message
If pos > 0 Then
'Debug.Print " Its a Match!!"
LocID = locIDs(pos)
Set destination = MkDirConditional(basefolder, LocID)
Debug.Print " " & Left(msg.Subject, 20), adress, pos, destination
msg.Move destination
Else
'Debug.Print " not found!"
End If
Else
'Debug.Print " " & "non-mailitem", itm.Subject
End If
Next i
Next fol
End Sub
'// Function - Check folder Exist
Private Function FolderExists(Inbox As Outlook.MAPIFolder, FolderName As String) As Boolean
Dim Sub_Folder As MAPIFolder
On Error GoTo Exit_Err
Set Sub_Folder = Inbox.Folders(FolderName)
FolderExists = True
Exit Function
Exit_Err:
FolderExists = False
End Function
Function MkDirConditional(basefolder As Outlook.MAPIFolder, newfolder As String) As Outlook.MAPIFolder
Debug.Print newfolder & " ";
If FolderExists(basefolder, newfolder) Then
'folder exists, so just skip
Set MkDirConditional = basefolder.Folders(newfolder)
Debug.Print "exists already"
Else
'folder doesnt exist, make it
Set MkDirConditional = basefolder.Folders.Add(newfolder)
Debug.Print "created"
End If
End Function
'function to compare two strings, min the option compare text at the top line
Function Findstring(str As String, arr As Variant) As Integer
'returns -1 if a string is not found, otherwise its index
Findstring = -1
Dim i As Integer
i = 1
For Each Item In arr
'Debug.Print Item
If str = Item Then
Findstring = i
Exit For
End If
i = i + 1
Next
End Function
' function to fetch the lists of emails and offices
Sub GetEMailsFolders(ByRef rng1 As Variant, ByRef rng2 As Variant, ByRef n As Variant)
'declare variables
Dim xExcelFile As String
Dim xExcelApp As Excel.Application
Dim xWb As Excel.Workbook
Dim xWs As Excel.Worksheet
Dim xExcelRange As Excel.Range
Dim TotalRows As Long
'declare SPOC xls file
xExcelFile = "adresses.xlsx"
'open the file
Set xExcelApp = CreateObject("Excel.Application")
Set xWb = xExcelApp.Workbooks.Open(xExcelFile)
Set xWs = xWb.Sheets(1)
'extract LocIDs (column A), emails (column O) and thir number
n = xWs.Range(xWs.Range("A2"), xWs.Range("A2").End(xlDown)).Count ' works
ReDim rng1(1 To n) As Variant
ReDim rng2(1 To n) As Variant
For i = 1 To n
rng1(i) = xWs.Cells(i + 1, 1)
rng2(i) = xWs.Cells(i + 1, 15)
'Debug.Print rng1(i), rng2(i)
Next
Debug.Print "done reading LocIDs & emails"
End Sub

Check e-mails in specific time frame

I need to check items in a folder in a specific time frame.
My code goes through all the mails in the specified folder, but the folder has thousands of mails, so it takes forever.
How do I check the mails only from, for example, 3/16/2015 12:00PM to 3/16/2015 2:00PM?
This is what I have:
Sub ExportToExcel()
Dim appExcel As Excel.Application
Dim wkb As Excel.Workbook
Dim wks As Excel.Worksheet
Dim rng As Excel.Range
Dim workbookFile As String
Dim msg As Outlook.MailItem
Dim nms As Outlook.NameSpace
Dim fld As Outlook.MAPIFolder
Dim itm As Object
'Folder path and file name of an existing Excel workbook
workbookFile = "C:\Users\OutlookItems.xls"
'Select export folder
Set nms = Application.GetNamespace("MAPI")
Set fld = nms.PickFolder
'Handle potential errors with Select Folder dialog box.
If fld Is Nothing Then
MsgBox "There are no mail messages to export", vbOKOnly, _
"Error"
Exit Sub
ElseIf fld.DefaultItemType <> olMailItem Then
MsgBox "There are no mail messages to export", vbOKOnly, _
"Error"
Exit Sub
ElseIf fld.Items.Count = 0 Then
MsgBox "There are no mail messages to export", vbOKOnly, _
"Error"
Exit Sub
End If
' Open and activate Excel workbook.
Set appExcel = CreateObject("Excel.Application")
Set wkb = appExcel.Workbooks.Open(workbookFile)
Set wks = wkb.Sheets(1)
wks.Activate
appExcel.Application.Visible = True
Set rng = wks.Range("A1")
'Copy field items in mail folder.
For Each itm In fld.Items
If itm.Class = Outlook.OlObjectClass.olMail Then
Set msg = itm
If InStr(msg.Subject, "Error in WU_Send") > 0 And DateDiff("h", msg.SentOn, Now) <= 2 Then
rng.Offset(0, 4).Value = msg.Body
Set rng = rng.Offset(1, 0)
End If
End If
Next
End Sub
The problem lies in this part:
For Each itm In fld.Items
If itm.Class = Outlook.OlObjectClass.olMail Then
Set msg = itm
If InStr(msg.Subject, "Error in WU_Send") > 0 And DateDiff("h", msg.SentOn, Now) <= 2 Then
How do I look at e-mails between specified hours?
You need to use the Find/FindNext or Restrict methods of the Items class instead of iterating through all items in the folder. For example:
Sub DemoFindNext()
Dim myNameSpace As Outlook.NameSpace
Dim tdystart As Date
Dim tdyend As Date
Dim myAppointments As Outlook.Items
Dim currentAppointment As Outlook.AppointmentItem
Set myNameSpace = Application.GetNamespace("MAPI")
tdystart = VBA.Format(Now, "Short Date")
tdyend = VBA.Format(Now + 1, "Short Date")
Set myAppointments = myNameSpace.GetDefaultFolder(olFolderCalendar).Items
Set currentAppointment = myAppointments.Find("[Start] >= """ & tdystart & """ and [Start] <= """ & tdyend & """")
While TypeName(currentAppointment) <> "Nothing"
MsgBox currentAppointment.Subject
Set currentAppointment = myAppointments.FindNext
Wend
End Sub
See the following articles for more information and sample code:
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
Also you may find the AdvancedSearch method of the Application class helpful. The key benefits of using the AdvancedSearch method are listed below:
The search is performed in another thread. You don’t need to run another thread manually since the AdvancedSearch method runs it automatically in the background.
Possibility to search for any item types: mail, appointment, calendar, notes etc. in any location, i.e. beyond the scope of a certain folder. The Restrict and Find/FindNext methods can be applied to a particular Items collection (see the Items property of the Folder class in Outlook).
Full support for DASL queries (custom properties can be used for searching too). You can read more about this in the Filtering article in MSDN. To improve the search performance, Instant Search keywords can be used if Instant Search is enabled for the store (see the IsInstantSearchEnabled property of the Store class).
You can stop the search process at any moment using the Stop method of the Search class.
You could just change the line to:
If InStr(msg.Subject, "Error in WU_Send") > 0 And msg.SentOn > "03/16/2015 12:00 PM" AND msg.SentOn < "03/16/2015 2:00 PM" Then
This specifies the time period.
Option Explicit
Sub RestrictTimePeriod()
Dim nms As Namespace
Dim fld As folder ' Subsequent to 2003 otherwise MAPIFolder
Dim msg As MailItem
Dim filterCriteria As String
Dim filterItems As Items
Dim i As Long
Dim start
Dim dif
Set nms = Application.GetNamespace("MAPI")
Set fld = nms.PickFolder
If Not fld Is Nothing Then
start = Now
Debug.Print start
' http://www.jpsoftwaretech.com/use-filters-to-speed-up-outlook-macros/
filterCriteria = "[ReceivedTime] > " & QuoteWrap("2015-03-16 12:00 PM") & _
" And [ReceivedTime] < " & QuoteWrap("2015-03-17 2:00 PM")
Set filterItems = fld.Items.Restrict(filterCriteria)
For i = filterItems.count To 1 Step -1
Set msg = filterItems.Item(i)
Debug.Print msg.Subject
Next
End If
ExitRoutine:
Set nms = Nothing
Set msg = Nothing
Set filterItems = Nothing
Debug.Print Now
dif = (Now - start) * 86400
Debug.Print dif
Debug.Print "Done."
End Sub
Function QuoteWrap(stringToWrap As String, _
Optional charToUse As Long = 39) As String
' http://www.jpsoftwaretech.com/use-filters-to-speed-up-outlook-macros/
' use 34 for double quotes, 39 for apostrophe
QuoteWrap = Chr(charToUse) & stringToWrap & Chr(charToUse)
End Function

Copy emails from Outlook folder to system folder

I am using the below VBA code in Outlook to copy the selected emails to system folder. Instead of selecting emails I need to modify the code to copy all the emails from a particular folder in Outlook.
' General Declarations
Option Explicit
' Public declarations
' Public Enum olSaveAsTypeEnum
'olSaveAsTxt = 0
'olSaveAsRTF = 1
'olSaveAsMsg = 3
'End Enum
Sub UATExport_MailasMSG()
' Routine will take all selected mails and export them as .MSG files to the
' directory defined by
' Error Handling
On Error Resume Next
' Varaiable Declarations
Dim objItem As Outlook.MailItem
Dim strExportFolder As String: strExportFolder = "I:\Documents\Dscan\"
Dim strExportFileName As String
Dim strExportPath As String
Dim objRegex As Object
Dim OldName As String, NewName As String
' Initiate regex search
Set objRegex = CreateObject("VBScript.RegExp")
With objRegex
.Pattern = "(\s|\\|/|<|>|\|\|\?|:)"
.Global = True
.IgnoreCase = True
End With
' Check if any objects are selected.
If Application.ActiveExplorer.Selection.Count = 0 Then
MsgBox ("No item has been selected.")
Else
' Cycle all selected objects.
For Each objItem In Application.ActiveExplorer.Selection
' If the currently selected item is a mail item we can proceed
If TypeOf objItem Is Outlook.MailItem Then
' Export to the predefined folder.
strExportFileName = objRegex.Replace(objItem.Subject, "_")
strExportPath = strExportFolder & strExportFileName & ".txt"
objItem.SaveAs strExportPath, olSaveAsTxt
'MsgBox ("Email saved to: " & strExportPath)
OldName = Dir(strExportPath)
NewName = Left(strExportPath, Len(strExportPath) - Len(OldName)) & _
Left(OldName, Len(OldName) - 4) & "DircanReportfor asmsmrwerwdb1u" & _
CStr(Format(FileDateTime(strExportPath), "ddmmyyhhmmss")) & ".txt"
Name strExportPath As NewName
' declaration to go with the others
Dim strEmailBodybackup As String
' this will go in your for loop
' Save the body so that we can restore it after.
strEmailBodybackup = objItem.Body
' Edit the body of the mail to suit needs.
objItem.Body = Replace(objItem.Body, "To", "Tscanfile", , 1, vbTextCompare)
' Process the export like in your question
' Restore the body of the original mail
objItem.Body = strEmailBodybackup
Else
' This is not an email item.
End If
Next 'objItem
End If
' Clear routine memory
Set objItem = Nothing
Set objRegex = Nothing
End Sub
The Folder class provides the Items property which returns an Items collection object as a collection of Outlook items in the specified folder. Be aware, the index for the Items collection starts at 1, and the items in the Items collection object are not guaranteed to be in any particular order. So, you can iterate over all items in the folder using the Items collection.
If you need to find a particular set of items that corresponds to the conditon, you can use the Find/FindNext or Restrict methods of the Items class.
The Sort method sorts the collection of items by the specified property. For example:
Sub SortByDueDate()
Dim myNameSpace As Outlook.NameSpace
Dim myFolder As Outlook.Folder
Dim myItem As Outlook.TaskItem
Dim myItems As Outlook.Items
Set myNameSpace = Application.GetNamespace("MAPI")
Set myFolder = myNameSpace.GetDefaultFolder(olFolderTasks)
Set myItems = myFolder.Items
myItems.Sort "[DueDate]", False
For Each myItem In myItems
MsgBox myItem.Subject & "-- " & myItem.DueDate
Next myItem
End Sub

VBA Filter only returning exactly half the restricted criteria items

I am writing some VBA for Outlook, which is not something I often do. I have a strange problem with the following code:
Sub Archive()
Dim objSourceFolder As Folder
Dim OldMessages As Outlook.Items
Dim Allmessages As Outlook.Items
Dim objMessage As MailItem
Dim dtDate As Date
Dim strDate As String
Dim strProblemFiles As String
Dim objTargetFolder As Outlook.MAPIFolder
'how old is too old? give a number in months
'-----------------------------------------------
Const iMonthAge = 6
'-----------------------------------------------
strProblemFiles = ""
'locate the sourcefolder as the inbox
Set objSourceFolder = Application.Session.GetDefaultFolder(olFolderInbox)
'locate the target folder as the only one that can work according to IT - they will make this folder consistent apparently
Set objTargetFolder = Application.Session.Folders.GetFirst
Set objTargetFolder = objTargetFolder.Folders("Archive")
'There is this crappy thing on the filtering of messages where it takes a date string, not a date. So we need to calculate and then convert
'to the format that MS lists on the MSDN site
dtDate = DateAdd("M", -iMonthAge, Now())
strDate = Format(dtDate, "ddddd h:nn AMPM")
'apply a filter to only show messages older than the specified date, which have been read.
Set Allmessages = objSourceFolder.Items
Set OldMessages = Allmessages.Restrict("[Received] <= '" & strDate & "' AND [Unread] = False")
'let the poor user know what's going on - they can bail out now if they want
If MsgBox("There are " & OldMessages.Count & " old items to archive. They will be moved from your " & objSourceFolder.Name & _
" folder to your " & objTargetFolder.Name & " folder.", vbYesNo, "Archive Files Now?") = vbYes Then
'go through all the messages in the big list of messages older than the specified date, moving them if possible.
For Each objMessage In OldMessages
If TypeName(OldMessages.GetFirst) = "MailItem" Then
'do our shizzle
Else
'PRETTY MINIMAL ERROR CATCHING NEEDS IMPROVING
'write down the name of anything that isn't mail, I guess... need to work on this
strProblemFiles = strProblemFiles + vbCrLf + objMessage.Subject
GoTo errorcatch
'GoTo CarryOn
End If
'make a note for anyone who can look
Debug.Print objMessage.Subject
If objTargetFolder.DefaultItemType = olMailItem Then
If objMessage.Class = olMail Then
'There's nothing in errorcatch, but there will be
On Error GoTo errorcatch
'Move the item if you can
objMessage.Move objTargetFolder
End If
End If
'after an error, we jump here to go to the noxt item
CarryOn:
Next
Else
'if the user doesn't want to do it, we need to shut up shop and get the hell out of here
Set objSourceFolder = Nothing
Set OldMessages = Nothing
Set objMessage = Nothing
Set objTargetFolder = Nothing
Exit Sub
End If
'now we have done the whole thing, we can wipe down for fingerprints and exit through the window
Set objSourceFolder = Nothing
Set OldMessages = Nothing
Set objMessage = Nothing
Set objTargetFolder = Nothing
'reset the errors
On Error GoTo 0
'probably not going to be any that weren't mail items, but didn't cause a real error, but I guess we should show any we skipped.
If strProblemFiles <> "" Then MsgBox strProblemFiles
Exit Sub
'pathetic
errorcatch:
GoTo CarryOn
End Sub
Function FileExists(FileName As String) As Boolean
FileExists = (Dir(FileName) <> "")
End Function
Everything works... nearly. the first time I run the macro, it tells me that there are (e.g. 128 items ready to archive. It runs and I notice that there are still old messages in my inbox, so I run it again and it tells me there are 64 items ready for archive... then 32, 16 etc. halving the number of found messages each time. I cannot see why it would do this. Any ideas?
I should mention that this is running on Outlook 2010, using an Exchange.
Thanks for looking - all answers most appreciated!
Cheers,
Mark
Something like:
'...
Dim colMove As New Collection
'...
For Each objMessage In OldMessages
If objTargetFolder.DefaultItemType = olMailItem Then
If objMessage.Class = olMail Then colMove.Add objMessage
End If
Next
For Each objMessage In colMove
objMessage.Move objTargetFolder
Next
'...
The For Each issue is explained, and another method to move or delete items counting backwards is described here.
For Each loop: Just deletes the very first attachment
Option Explicit
Sub Archive()
Dim objSourceFolder As Folder
Dim OldMessages As Outlook.Items
Dim AllMessages As Outlook.Items
Dim objMessage As Object
Dim dtDate As Date
Dim strDate As String
Dim strProblemFiles As String
Dim objTargetFolder As Outlook.MAPIFolder
Dim colMove As New Collection
Dim objFolder As Outlook.MAPIFolder
Dim lngSize As Long
Dim objAnything As Object
Dim iMaxMBSize As Integer
Dim boolSentItems As Boolean
Dim catCategory As category
' Dim boolCatExists As Boolean
' Dim iColour As Integer
Set objSourceFolder = Nothing
Set OldMessages = Nothing
Set objMessage = Nothing
Set objTargetFolder = Nothing
'iColour = 18
'we are moving files, that's all, so we don't really need to worry too much about errors - if there is a problem, we can just skip the file
'without great negative effects.
On Error Resume Next
'how old is too old? give a number in months
'-----------------------------------------------
Const iMonthAge = 6
iMaxMBSize = 50
'-----------------------------------------------
'locate the sourcefolder as the inbox
boolSentItems = (MsgBox("Your inbox will be archived." & vbCrLf & _
"Do you want to also archive sent items?", vbYesNo, "Archive Options") = vbYes)
Set objSourceFolder = Application.Session.GetDefaultFolder(olFolderInbox)
'----------------------------------------------------------------------------------------------------------------------------------------
StartAgain:
'If you wish to assign a category to the folders rather than keep the folder structure when you archive, use this code and some other bits
'later on, which mention the categories and the variables mentioned here.
'Set objSourceFolder = Application.Session.GetDefaultFolder(olFolderInbox)
' boolCatExists = False
'For Each catCategory In Application.Session.Categories
' If catCategory.Name = "Archived from " & objSourceFolder.Name Then
' boolCatExists = True
' End If
'Next
'If boolCatExists = False Then
' Application.Session.Categories.Add "Archived from " & objSourceFolder.Name, iColour
'End If
'locate the target folder, which must be either in the same level as the inbox or lower
'----------------------------------------------------------------------------------------------------------------------------------------
Set objTargetFolder = SearchFolders(objSourceFolder.Parent, "Archive")
'if the target folder was not found, then we need to make it, in the root directory (the same level as the inbox - this is stipulated by IT)
If objTargetFolder Is Nothing Then
Set objTargetFolder = Application.Session.GetDefaultFolder(olFolderInbox).Folders.Add("Archive")
End If
'we are going to maintain the folder structure in the archive folder, for the inbox and sent items. This means we know exactly what to look for. If it isn't there,
'we just create it. I have used the search, rather than specifying the folders so that if the archiving is extended to more than just the inbobx and sent items, no
'change is needed.
If SearchFolders(objTargetFolder, objSourceFolder.Name) Is Nothing Then
Set objTargetFolder = objTargetFolder.Folders.Add(objSourceFolder.Name)
Else
Set objTargetFolder = objTargetFolder.Folders(objSourceFolder.Name)
End If
'There is this crappy thing on the filtering of messages where it takes a date string, not a date. So we need to calculate and then convert
'to the format that MS lists on the MSDN site
dtDate = DateAdd("M", -iMonthAge, Now())
strDate = Format(dtDate, "ddddd h:nn AMPM")
'apply a filter to only show messages older than the specified date, which have been read.
Set OldMessages = objSourceFolder.Items
Set OldMessages = OldMessages.Restrict("[Received] <= '" & strDate & "' AND [Unread] = False")
'let the poor user know what's going on - they can bail out now if they want
If OldMessages.Count > 0 Then
' If MsgBox("There are " & OldMessages.Count & " old items in your " & objSourceFolder.Name & ". Do you want to move them from your " & objSourceFolder.Name & _
' " folder to your " & objTargetFolder.Name & " folder.", vbYesNo, UCase(objSourceFolder.Name) + " Archive") = vbYes Then
'----------------------------------------------------------------------------------------------------------------------------------------
'go through all the messages in the big list of messages older than the specified date, moving them if possible.
'StatusForm.Show vbModeless
For Each objMessage In OldMessages
If TypeName(objMessage) = "MailItem" Then
'do our shizzle
Else
'if it is not a mailitem, there may be problems moving it - add it to the list instead.
strProblemFiles = strProblemFiles + vbCrLf + objSourceFolder.Name + ": " + objMessage.Subject
End If
'make a note for anyone who can look
Debug.Print objMessage.Subject
'probably pointless since we are only looking in the inbox and sent items, and making the mirrors ourselves, but check the folder is correct
If objTargetFolder.DefaultItemType = olMailItem Then
If objMessage.Class = olMail Then
'put the message in a nice stable collection for now - that way, we don't have to worry about the count changing etc
colMove.Add objMessage
End If
End If
Next objMessage
'----------------------------------------------------------------------------------------------------------------------------------------
'and here we have the actual move (and some optional text if you are using the categories)
For Each objMessage In colMove
'Move the item if you can
'objMessage.Categories = "Archived from " & objSourceFolder.Name
'objMessage.Save
objMessage.Move objTargetFolder
Next objMessage
'----------------------------------------------------------------------------------------------------------------------------------------
'Else
' 'if the user doesn't want to do it, we need to shut up shop and get the hell out of here
' Set objSourceFolder = Nothing
' Set OldMessages = Nothing
' Set objMessage = Nothing
' Set objTargetFolder = Nothing
' Exit Sub
'End If
Else
'if the count of all the old messages is not greater than 0
MsgBox "There are no messages from more than " & iMonthAge & " months ago in your " & objTargetFolder.Name & _
", so nothing will be archived.", vbExclamation, "Mailbox is Clean"
End If
'----------------------------------------------------------------------------------------------------------------------------------------
'finally, loop through literally all the items in the target folders and add up the sizes to see how much we have archived in total.
For Each objAnything In objTargetFolder.Parent.Items
lngSize = lngSize + objAnything.size
Next
'if they want to include the sent items in the archive, then change over the folder and do it all again
If boolSentItems = True Then
boolSentItems = False
Set objSourceFolder = SearchFolders(objSourceFolder.Parent, "Sent Items")
'iColour = iColour + 1
GoTo StartAgain
End If
'----------------------------------------------------------------------------------------------------------------------------------------
'once we have done all we can, let the user know about all the files that were skipped.
If strProblemFiles <> "" Then
MsgBox "The following items were skipped, so will still be in your mailbox" & vbCrLf & strProblemFiles, vbOKOnly, "Non-Mail Items"
Else
MsgBox "Archive complete", vbOKOnly, "Files Moved"
End If
'----------------------------------------------------------------------------------------------------------------------------------------
'the size of each file is listed in Bytes, so convert to MB to check the MB size and display, for convenience.
If lngSize / (1024 ^ 2) >= iMaxMBSize Then
MsgBox "Your archive folder takes up " & Round(lngSize / (1024 ^ 2), 0) & "MB; it is time to call IT to ask them to clear out the files", vbOKOnly, _
"Archive folder bigger than " & iMaxMBSize & "MB"
End If
'now we have done the whole thing, we can wipe down for fingerprints and exit through the window
Set objSourceFolder = Nothing
Set OldMessages = Nothing
Set objMessage = Nothing
Set objTargetFolder = Nothing
StatusForm.Hide
On Error GoTo 0
Exit Sub
'ErrorCatch:
'If you decide to add some error checking, put it in here, although as I say, I haven't bothered (see Declaration section at top)
End Sub
Public Function SearchFolders(objTopFolder As Outlook.MAPIFolder, strName As String)
Dim objFolder As Outlook.MAPIFolder
'look through all the sub folders at the level we started
For Each objFolder In objTopFolder.Folders
'If we find the one that we are looking for, great! we can get it and get out
If objFolder.Name = strName Then
Set SearchFolders = objFolder
Exit Function
'if we haven't found our magic folder yet, we need to carry on, by looking for any sub-sub folders this is done by calling the function itself on
'the current folder (which is by definition already one level lower than the starting location). if nothing is found, we,ll just carry on
Else
If objFolder.Folders.Count > 0 Then
Call SearchFolders(objFolder, strName)
End If
End If
Next
'the only way to exit the loop at this point is if all the folders have been searched and the folder we were looking for was not found.
Set SearchFolders = Nothing
End Function
the "StatusForm" user form that is referred to is a completely static form that just says "Archiving..." so the user is less likely to try mucking around in Outlook while the macro runs.