Mass Export outlook letters with ConversationID - vba

I need to extract all emails with standard outlook fields (from/to/subject/date, including category and most importantly, ConversationID) into Excel/csv.
I'm using MS Office 2016, no idea about version of Exchange server.
I tried several ways to do so on my mailbox:
1) exported data through standard outlook interface
2) exported data into MS access via standard export master
3) extracted data to MS PowerBI from MS Exchange directly
In all 3 cases I wasn't able to get ConversationID (PowerBI extract had some ID but it was not ConversationID)
Now I understand that it should be extracted through MAPI somehow, but I'm totally illiterate on this topic. Some searches advised to use special software for that, like Transcend, but it's obviously too expensive for one user :)
I also found VBA code to get data into Excel directly but it is not working for me:
http://www.tek-tips.com/viewthread.cfm?qid=1739523
Also found this nice explanation what is ConversationID - might be helpful for others intrested in topic:
https://www.meridiandiscovery.com/how-to/e-mail-conversation-index-metadata-computer-forensics/

Here is some sample code to get you started, I already had something similar to your ask. The code is commented, but feel free to ask questions :)
Option Explicit
Public Sub getEmails()
On Error GoTo errhand:
'Create outlook objects and select a folder
Dim outlook As Object: Set outlook = CreateObject("Outlook.Application")
Dim ns As Object: Set ns = outlook.GetNameSpace("MAPI")
'This option open a new window for you to select which folder you want to work with
Dim olFolder As Object: Set olFolder = ns.pickFolder
Dim emailCount As Long: emailCount = olFolder.Items.Count
Dim i As Long
Dim myArray As Variant
Dim item As Object
ReDim myArray(4, (emailCount - 1))
For i = 1 To emailCount
Set item = olFolder.Items(i)
'43 is olMailItem, only consider this type of email message
'I'm assuming you only want items with a conversationID
'Change the logic here to suite your specific needs
If item.Class = 43 And item.ConversationID <> vbNullString Then
'Using an array to write to excel in one go
myArray(0, i - 1) = item.Subject
myArray(1, i - 1) = item.SenderName
myArray(2, i - 1) = item.To
myArray(3, i - 1) = item.CreationTime
myArray(4, i - 1) = item.ConversationID
End If
Next
'Adding headers, then writing the data to excel
With ActiveSheet
.Range("A1") = "Subject"
.Range("B1") = "From"
.Range("C1") = "To"
.Range("D1") = "Created"
.Range("E1") = "ConversationID"
.Range("A2:E" & (emailCount + 1)).Value = TransposeArray(myArray)
End With
Exit Sub
errhand:
Debug.Print Err.Number, Err.Description
End Sub
'This function is used to bypass the limitation of -
'application.worksheetfunction.transpose
'If you have too much data added to an array you'll get a type mismatch
'Found here - http://bettersolutions.com/vba/arrays/transposing.htm
Public Function TransposeArray(myArray As Variant) As Variant
Dim X As Long
Dim Y As Long
Dim Xupper As Long: Xupper = UBound(myArray, 2)
Dim Yupper As Long: Yupper = UBound(myArray, 1)
Dim tempArray As Variant
ReDim tempArray(Xupper, Yupper)
For X = 0 To Xupper
For Y = 0 To Yupper
tempArray(X, Y) = myArray(Y, X)
Next
Next
TransposeArray = tempArray
End Function

Related

dictionary.exists(key) ADDS the key

I am going crazy with VBA dictionaries, as the Exists() method makes no sense.
I thought you can use the dict.Exists(key) method to check if a key is in the dictionary without further actions. The problem is that when checking it, the key is automatically added into the dictionary. It really makes no sense!
Here's my code. Am I doing something wrong?
Function getContracts(wb As Workbook) As Dictionary
Dim cData As Variant, fromTo(1 To 2) As Variant
Dim contracts As New Dictionary, ctrDates As New Collection
Dim positions As New Dictionary, p As Long, r As Long
Dim dataSh As String, i As Long
dataSh = "Export"
cData = wb.Worksheets(dataSh).UsedRange
For i = LBound(cData) To UBound(cData)
fromTo(1) = cData(i, 1)
fromTo(2) = cData(i, 2)
Set ctrDates = Nothing
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not contracts.Exists(cData(i, 3)) Then ' Here it detects correctly that the key doesn't exist
ctrDates.Add fromTo
contracts.Add cData(i, 3), ctrDates ' And here it fails because the key just got added by .Exists()
Else
Set ctrDates = contracts(cData(i, 3))
ctrDates.Add fromTo
contracts(cData(i, 3)) = ctrDates
End If
Else
Debug.Print "Not a valid date in line " & i
End If
Next i
End Function
You can shorten your code to
For i = LBound(cData) To UBound(cData)
fromTo(1) = cData(i, 1)
fromTo(2) = cData(i, 2)
Set ctrDates = Nothing
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not IsEmpty(contracts(cData(i, 3))) Then Set ctrDates = contracts(cData(i, 3))
ctrDates.Add fromTo
Set contracts(cData(i, 3)) = ctrDates
Else
Debug.Print "Not a valid date in line " & i
End If
Next i
If one changes a value at a key it will automatically add the key if it does not exist.
Further reading on dictionaries
PS: This might also circumvent the strange behaviour described in the comments as you do not use the exist method. But on the other hand I have never experienced such a strange behaviour when using dictionaries
Collections of Date Pairs in a Dictionary
A reference to the Microsoft Scripting Runtime library is necessary for this to work.
Option Explicit
Sub GetContractsTEST()
Const dName As String = "Export"
Dim wb As Workbook: Set wb = ThisWorkbook
Dim dws As Worksheet: Set dws = wb.Worksheets(dName)
Dim Contracts As Scripting.Dictionary: Set Contracts = GetContracts(dws)
If Contracts Is Nothing Then Exit Sub
Dim Key As Variant, Item As Variant
For Each Key In Contracts.Keys
Debug.Print Key
For Each Item In Contracts(Key)
Debug.Print Item(1), Item(2)
Next Item
Next Key
End Sub
Function GetContracts(ByVal ws As Worksheet) As Scripting.Dictionary
Const ProcName As String = "GetContracts"
On Error GoTo ClearError
Dim cData As Variant: cData = ws.UsedRange.Value
Dim fromTo(1 To 2) As Variant
Dim Contracts As New Scripting.Dictionary
Contracts.CompareMode = TextCompare
Dim r As Long
For r = LBound(cData) To UBound(cData)
fromTo(1) = cData(r, 1)
fromTo(2) = cData(r, 2)
If IsDate(fromTo(1)) And IsDate(fromTo(2)) Then
If Not Contracts.Exists(cData(r, 3)) Then
Set Contracts(cData(r, 3)) = New Collection
End If
Contracts(cData(r, 3)).Add fromTo
Else
Debug.Print "Not a valid date in line " & r
End If
Next r
Set GetContracts = Contracts
ProcExit:
Exit Function
ClearError:
Debug.Print "'" & ProcName & "' Run-time error '" _
& Err.Number & "':" & vbLf & " " & Err.Description
Resume ProcExit
End Function
Possible Solution:
I had the same issue, this tends to happen when the compare more has not been set. I have not dug any deeper into this as the issue cannot always be replicated and the documentation around .Exists() and .CompareMode isn't that thorough source.
(as everyone has said you should enable the Microsoft Scripting Runtime reference for early binding)
When creating a new dictionary set its .CompareMode to vbBinaryCompare this will set a more strict compare mode and also in my case fixes the .Exists() bug. Do note that you can only set .CompareMode on an empty dictionary
Dim NewDictionary As New Scripting.Dictionary
NewDictionary.CompareMode = vbBinaryCompare
If NewDictionary.Exists(key) Then
'do things
End If

Object or With Variable Not Set

Option Explicit
Public Sub consolidateList()
DeleteTableRows (ThisWorkbook.Worksheets("Master").ListObjects("MasterSheet"))
FillTableRows
End Sub
Private Sub FillTableRows()
'set up worksheet objects
Dim wkSheet As Worksheet
Dim wkBook As Workbook
Dim wkBookPath As String
Set wkBook = ThisWorkbook
wkBookPath = wkBook.Path
Set wkSheet = wkBook.Worksheets("Master")
'set up file system objects
Dim oFile As Object
Dim oFSO As Object
Dim oFolder As Object
Dim oFiles As Object
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFolder = oFSO.GetFolder(wkBookPath)
Set oFiles = oFolder.Files
'set up loop
Dim checkBook As Excel.Workbook
Dim reportDict As Dictionary
Application.ScreenUpdating = False
'initial coordinates
Dim startRow As Long
Dim startColumn As Long
startColumn = 3
Dim i As Long 'tracks within the row of the sheet where information is being pulled from
Dim k As Long 'tracks the row where data is output on
Dim j As Long 'tracks within the row of the sheet where the data is output on
Dim Key As Variant
j = 1
k = wkSheet.Range("a65536").End(xlUp).Row + 1
Dim l As Long
'look t Set checkBook = Workbooks.Open(oFile.Path)hrough folder and then save it to temp memory
On Error GoTo debuger
For Each oFile In oFiles
startRow = 8
'is it not the master sheet? check for duplicate entries
'oFile.name is the name of the file being scanned
'is it an excel file?
If Mid(oFile.Name, Len(oFile.Name) - 3, 4) = ".xls" Or Mid(oFile.Name, Len(oFile.Name) - 3, 4) = ".xlsx" Then
Set checkBook = Workbooks.Open(oFile.Path)
For l = startRow To 600
If Not (IsEmpty(Cells(startRow, startColumn))) Then
'if it is, time do some calculations
Set reportDict = New Dictionary
'add items of the payment
For i = 0 To 33
If Not IsEmpty(Cells(startRow, startColumn + i)) Then
reportDict.Add Cells(4, startColumn + i), Cells(startRow, startColumn + i)
End If
Next i
For i = startRow To 0 Step -1
If Not IsEmpty(Cells(i, startColumn - 1)) Then
reportDict.Add "Consumer Name", Cells(i, startColumn - 1)
Exit For
End If
Next i
'key is added
For Each Key In reportDict
'wkSheet.Cells(k, j) = reportDict.Item(Key)
Dim myInsert As Variant
Set myInsert = reportDict.Item(Key)
MsgBox (myInsert)
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
j = j + 1
Next Key
wkSheet.Cells(k, j) = wkSheet.Cells(k, 9) / 4
wkSheet.Cells(k, j + 1) = oFile.Name
'
k = k + 1
' Set reportDict = Nothing
j = 1
Else
l = l + 1
End If
startRow = startRow + 1
Next l
checkBook.Close
End If
' Exit For
Next oFile
Exit Sub
debuger:
MsgBox ("Error on: " & Err.Source & " in file " & oFile.Name & ", error is " & Err.Description)
End Sub
Sub DeleteTableRows(ByRef Table As ListObject)
On Error Resume Next
'~~> Clear Header Row `IF` it exists
Table.DataBodyRange.ClearContents
'~~> Delete all the other rows `IF `they exist
Table.DataBodyRange.Offset(1, 0).Resize(Table.DataBodyRange.Rows.count - 1, _
Table.DataBodyRange.Columns.count).Rows.Delete
On Error GoTo 0
End Sub
Greetings. The above code consolidates a folder of data that's held on excel spreadsheets into one master excel spreadsheet. The goal is to run a macro on Excel Spreadsheet named master on the worksheet named master which opens up other excel workbooks in the folder, takes the information, and puts it into a table in the worksheet "master". After which point, it becomes easy to see the information; so instead of it being held on hundreds of worksheets, the records are held on one worksheet.
The code uses a dictionary (reportDict) to temporarily store the information that is needed from the individual workbooks. The goal then is to take that information and place it in the master table at the bottom row, and then obviously add a new row either after a successful placement or before an attempted placement of data.
The code fails at the following line:
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
The failure description is "object or with variable not set" and so the issue is with the reportDict.Item(Key). My guess is that somehow VBA is not recognizing the dictionary item as stable, but I don't know how to correct this. Eventually the goal is to have code which does:
for each key in reportDict
- place the item which is mapped to the key at a unique row,column in the master table
- expand the table to accomodate necessary data
next key
Implicit default member calls are plaguing your code all over.
reportDict.Add Cells(4, startColumn + i), Cells(startRow, startColumn + i)
That's implicitly accessing Range.[_Default] off whatever worksheet is currently the ActiveSheet (did you mean that to be wkSheet.Cells?), to get the Key - since the Key parameter is a String, Range.[_Default] is implicitly coerced into one, and you have a string key. The actual dictionary item at that key though, isn't as lucky.
Here's a MCVE:
Public Sub Test()
Dim d As Dictionary
Set d = New Dictionary
d.Add "A1", Cells(1, 1)
Debug.Print IsObject(d("A1"))
End Sub
This procedure prints True to the debug pane (Ctrl+G): what you're storing in your dictionary isn't a bunch of string values, but a bunch of Range object references.
So when you do this:
Dim myInsert As Variant
Set myInsert = reportDict.Item(Key)
You might as well have declared myInsert As Range, for it is one.
This is where things get interesting:
MsgBox (myInsert)
Nevermind the superfluous parentheses that force-evaluate the object's default member and pass it ByVal to the MsgBox function - here you're implicitly converting Range.[_Default] into a String. That probably works.
So why is this failing then?
wkSheet.ListObjects(1).DataBodyRange(2, 1) = reportDict.Item(Key)
Normally, it wouldn't. VBA would happily do this:
wkSheet.ListObjects(1).DataBodyRange.Cells(2, 1).[_Default] = reportDict.Item(Key).[_Default]
And write the value in the DataBodyRange of the ListObject at the specified location.
I think that's all just red herring. Write explicit code: if you mean to store the Value of a cell, store the Value of a cell. If you mean to assign the Value of a cell, assign the Value of a cell.
I can't replicate error 91 with this setup.
This, however:
DeleteTableRows (ThisWorkbook.Worksheets("Master").ListObjects("MasterSheet"))
...is also force-evaluating a ListObject's default member - so DeleteTableRows isn't receiving a ListObject, it's getting a String that contains the name of the object you've just dereferenced... but DeleteTableRows takes a ListObject parameter, so there's no way that code can even get to run FillTableRows - it has to blow up with a type mismatch before DeleteTableRows even gets to enter. In fact, it's a compile-time error.
So this is a rather long answer that doesn't get to the reason for error 91 on that specific line (I can't reproduce it), but highlights a metric ton of serious problems with your code that very likely are related to this error you're getting. Hope it helps.
You need to iterate through the dictionary's Keys collection.
dim k as variant, myInsert As Variant
for each k in reportDict.keys
debug.print reportDict.Item(k)
next k

Script to pull Outlook GAL in Excel

I am using the following script to pull in the fields I need for a project from the Global Address Book in excel, and it is functioning properly, but I would like to add a field that includes the floor number that an individual sits on. Does anyone know how to add this field? I have tried all of the fields withing the GetExchangeUser object group. Please let me know! I would be very grateful!!
Sub GetOutlookAddressBook()
' Need to add reference to Outlook
'(In VBA editor Tools References MS Outlook #.# Library)
' Adds addresses to existing Sheet called Address and
' defines name Addresses containing this list
' For use with data Validation ListBox (Source as =Addresses)
On Error GoTo 0
Dim objOutlook As Outlook.Application
Dim objAddressList As Outlook.AddressList
Dim objAddressEntry As Outlook.AddressEntry
Dim lngCounter As Long
Application.ScreenUpdating = False
' Setup connection to Outlook application
Set objOutlook = CreateObject("Outlook.Application")
Set objAddressList = objOutlook.Session.AddressLists("Global Address List")
Application.EnableEvents = False
'Application.DisplayAlerts = False
' Clear existing list
Sheets("Address").Range("A:A").Clear
'Step through each contact and list each that has an email address
For Each objAddressEntry In objAddressList.AddressEntries
If objAddressEntry.AddressEntryUserType = olExchangeUserAddressEntry Then
lngCounter = lngCounter + 1
Application.StatusBar = "Address no. " & lngCounter & " ... " & objAddressEntry.Address
Sheets("Address").Cells(lngCounter, 1) = objAddressEntry.GetExchangeUser.Alias
Sheets("Address").Cells(lngCounter, 2) = objAddressEntry.GetExchangeUser.Name
Sheets("Address").Cells(lngCounter, 3) = objAddressEntry.GetExchangeUser.CompanyName
Sheets("Address").Cells(lngCounter, 4) = objAddressEntry.GetExchangeUser.Address
Sheets("Address").Cells(lngCounter, 5) = objAddressEntry.GetExchangeUser.Department
Sheets("Address").Cells(lngCounter, 6) = objAddressEntry.GetExchangeUser.JobTitle
Sheets("Address").Cells(lngCounter, 7) = objAddressEntry.GetExchangeUser.OfficeLocation
DoEvents
End If
Next objAddressEntry
' Define range called "Addresses" to the list of emails
'Sheets("Address").Cells(1, 1).Resize(lngCounter, 1).Name = "Addresses"
'error:
Set objOutlook = Nothing
Application.StatusBar = False
Application.EnableEvents = False
End Sub
Thanks!!
Lacey
.OfficeLocation is about it :), there is no floor number property available.

Slowing down vba to take into account network lag (outlook)

I've written a script to help me a little with resource planning. It's looking through a shared outlook inbox to find out when we receive the most emails, and so when we should avoid any breaks etc.
It seems to be working perfectly, but occasionally throws up a random error (type mismatch, call failed, operation failed etc). When I run the debugger from the error message it carries on as normal. It gets through around 800ish messages each time, between errors, and more if it's in a good mood.
All I can think is that sometimes the different subfolders take a moment to load. My code is below, is there anything that I can add to make it wait for a moment for messages to load from the server?
Thanks in advance.
Calling loop in the sub:
For Each msg In StartFolder.Items
DoEvents
msgData = ripData(msg)
written = toExcel(msgData, strExcelFilePath)
Next
Functions defined below:
Function ripData(msg As Outlook.MailItem) As Variant
Dim V() As Variant
ReDim V(1 To 10)
Dim minutes As Integer
DoEvents
V(1) = msg.Sender
If InStr(1, msg.Sender.Address, "#", 1) > 1 Then
V(2) = Mid(msg.Sender.Address, InStr(1, msg.Sender.Address, "#", 1))
Else
V(2) = "insight.com"
End If
V(3) = Format(msg.ReceivedTime, "short date")
V(4) = Format(msg.ReceivedTime, "DDDD")
V(5) = Format(msg.ReceivedTime, "dd")
V(6) = Format(msg.ReceivedTime, "MMMM")
V(7) = Format(msg.ReceivedTime, "yyyy")
V(8) = Format(msg.ReceivedTime, "hh:mm")
V(9) = Format(msg.ReceivedTime, "hh")
minutes = Split(Format(msg.ReceivedTime, "hh:mm"), ":")(1)
If minutes < 15 Then
V(10) = 1
ElseIf minutes < 30 Then
V(10) = 2
ElseIf minutes < 45 Then
V(10) = 3
Else
V(10) = 4
End If
ripData = V
End Function
Function toExcel(data As Variant, excelFName As String) As Boolean
Dim fso As New FileSystemObject
Dim spath As String, sFileName As String, fileWithoutExt As String, lrow As Long
Dim i As Long
Dim myWB As Object, oXLWs As Object
sFileName = fso.GetFileName(excelFName)
fileWithoutExt = sFileName
Set myWB = FindOpenExcel(excelFName, fileWithoutExt, sFileName)
Set oXLWs = myWB.Sheets("Raw Data")
lrow = oXLWs.Range("A1048576").End(xlUp).Row + 1
'~~> Write to excel
For i = 1 To UBound(data)
oXLWs.Cells(lrow, i).Value = data(i)
Next i
End Function
Type mismatch means you are assuming that all items are MailItem object, but the Inbox folder can have other item typed ReportItem, MeetingItem, etc.). Do check the Class property ot make sure you have the expected object - it will be 43 (olMail) for the MailItem objects.
Secondly, you might be opening too many items - Exchange limits the number of simultaneously open objects. Plus "for each" loop keeps all collection elements referenced until the loop exits. Use the "for" loop
dim oItems
oItems = StartFolder.Items
dim I As Integer
dim msg As Object
For I = 1 to oItems.Count
set msg = oItems.Item(I)
if msg.Class = 43 Then
msgData = ripData(msg)
written = toExcel(msgData, strExcelFilePath)
End If
set msg = Nothing
Next
If caching is not set on (account settings) for the shared folders you can get operation failed errors intermittently.

Read Cell properties in Visio using vb

I am trying to create a VB macro in Visio that can read the data and properties of the shape. So say I have a Rectangle Shpae in Visio with Cells Name, Description, Type, Size.... and so on.
When I try to read the cells and their values I am only getting the first cell and its value.
Here is my code . I would appreciate some help here.
Sub Testing()
Dim excelObj As Object
Dim excelFile As String
Dim sheetName As String
' Dim excelBook As Excel.Workbook
' Set excelFile = "C:\Users\hbhasin\Documents\test.xls"
'Set sheetName = "New Sheet name"
Set excelObj = CreateObject("Excel.Application")
excelObj.Workbooks.Add
Dim pagObj As Visio.Page
Dim shpsObj As Visio.shapes
Dim shapes As Visio.shapes
Dim shpObj As Visio.Shape
Dim CellObj As Visio.Cell
Dim Storage() As String
Dim iShapeCount As Integer
Dim i As Integer
Dim j As Integer
Set pagObj = ActivePage
Set shpsObj = pagObj.shapes
iShapeCount = shpsObj.Count
Debug.Print iShapeCount
ReDim Storage(8, iShapeCount - 1)
For i = 1 To iShapeCount - 1
Set shpObj = shpsObj(i)
Storage(1, i - 1) = shpObj.Name
If shpObj.CellExists("Prop.Name", visExistsLocally) Then
Set CellObj = shpObj.CellsU("Prop.Name")
Storage(2, i - 1) = CellObj.ResultStr("")
End If
If shpObj.CellExists("Prop.Description", visExistsLocally) Then
Debug.Print "Test the IF statement"
Set CellObj = shpObj.CellsU("Prop.Description")
Storage(3, i - 1) = CellObj.ResultStr("")
End If
Next
For i = 0 To iShapeCount - 1
Debug.Print "Name- " & Storage(0, i)
Debug.Print "Description-" & Storage(1, i)
Next
End Sub
In fact, I have put a debug statement within the second if clause and that does not execute which tells me the compiler is not even seeing the second cell or any cell after.
If you're not getting the Description Shape Data it maybe that it's not local, but inherited from its master. Here's a slight modification of your code (with the Excel part removed as I don't think it's relevant here):
Sub Testing()
Dim shpsObj As Visio.shapes
Set shpsObj = ActivePage.shapes
Dim iShapeCount As Integer
iShapeCount = shpsObj.Count
'Assumes you want an array of shape data
Dim Storage() As String
ReDim Storage(iShapeCount - 1, 2)
'Visio shapes are 1 based so use full count
Dim i As Integer
Dim shpObj As Visio.Shape
For i = 1 To iShapeCount
Set shpObj = shpsObj(i)
Storage(i - 1, 0) = shpObj.Name 'Do you want NameU?
'Assumes you don't care whether the cell is local or inherited
If shpObj.CellExistsU("Prop.Name", visExistsAnywhere) Then
Storage(i - 1, 1) = shpObj.CellsU("Prop.Name").ResultStr("")
End If
If shpObj.CellExistsU("Prop.Description", visExistsAnywhere) Then
Storage(i - 1, 2) = shpObj.CellsU("Prop.Description").ResultStr("")
End If
Next
Dim j As Long
For j = LBound(Storage, 1) To UBound(Storage, 1)
Debug.Print "Shape Name- " & Storage(j, 0)
Debug.Print " Prop.Name- " & Storage(j, 1)
Debug.Print " Prop.Description- " & Storage(j, 2)
Next j
End Sub
If you're just running through all the shapes on the page, then you might want to look at For Each shp In shapes as an alternative. Check out this page for more details:
http://visualsignals.typepad.co.uk/vislog/2007/11/looping-through.html
Also, you might want to try look at the CreateSelection page method to narrow down your target shapes if you're dealing with a large number