Calendar appointments

Updated on 3/1/2024

Should you be using the Outlook calendar, this script extracts useful information about your appointments, which you can analyze to gain insight on how you spent your time by project, time period, the persons you interact most, etc.

Specifically, it scans your calendar entries and assembles the list of all your appointments.

By the way, there is a relevant article that extracts information about your Outlook mails.

For each appointment, it will accumulate a variety of properties, such as start, end, duration, subject, category, attendees, etc. There’s the full list of all properties:

AllDayEvent, AutoResolvedWinner, BillingInformation, BusyStatus, Categories, Class, Companies, ConversationIndex, ConversationTopic, CreationTime, DownloadState, Duration, End, EndInEndTimeZone, EndTimeZone, EndUTC, EntryID, ForceUpdateToAllAttendees, FormDescription, GlobalAppointmentID, Importance, InternetCodepage, IsConflict, IsRecurring, LastModificationTime, Location, MarkForDownload, MeetingStatus, MeetingWorkspaceURL, MessageClass, Mileage, NoAging, OptionalAttendees, Organizer, OutlookInternalVersion, OutlookVersion, RecurrenceState, ReminderMinutesBeforeStart, ReminderOverrideDefault, ReminderPlaySound, ReminderSet, ReminderSoundFile, ReplyTime, RequiredAttendees, Resources, ResponseRequested, ResponseStatus, Saved, Sensitivity, Size, Start, StartTimeZone, StartUTC, Subject, UnRead

The information for the appointments is stored in a Comma Separated File, ready to be fed to your Excel, or favorite data analytics tool (mine was Power BI, but now I reverted back to MS Access). A sample is shown below, where I have left visible the most important properties.

You can press Alt-F11 in Outlook to open the VBA and Insert | Module for a clean editor window. Then copy and paste the code below into an empty Outlook module. Place your cursor anywhere into the CalendarAppointments subroutine and press F5 to run it.

Option Explicit

' CalendarAppointments() gets all appointments with their detailed field properties
'                        from Outlook Calendar and saves them in a CSV file
' Code originates from http://www.GregThatcher.com
' Edited by I Gaviotis, igaviotis.wordpress.com

' Version 2023-07-06 added explanatory comments on use
' Version 2024-01-03 added explanatory comments and edited for easy read

' Paste the VBA code in an Outlook module (to open new module, Alt-F11).
' Run CalendarAppointments() by pressing F5 on its body. 
' Outputs CSV file, saved on Desktop to find easily.
' Observe results in Excel.

Public Sub CalendarAppointments()
    On Error GoTo ErrorHandler
    
    Dim objSession As Outlook.NameSpace
    Dim objAppointmentsFolder As Outlook.Folder
    Set objSession = Application.Session
    Set objAppointmentsFolder = objSession.GetDefaultFolder(olFolderCalendar)
    

    Dim strOutputFilename As String, outFile As Integer
    strOutputFilename = "CalendarAppointments.csv" 'creates file in Desktop folder
    outFile = FreeFile
    Open strOutputFilename For Output As #outFile
    
    Debug.Print "CalendarAppointments Start " & Now
    Debug.Print "Appointments will be saved in " & strOutputFilename & ".."
    
    Dim arrFields As Variant
    arrFields = Array( _
		"AllDayEvent", "AutoResolvedWinner", "BillingInformation", "BusyStatus", _
		"Categories", "Class", "Companies", "ConversationIndex", "ConversationTopic", _
		"CreationTime", "DownloadState", "Duration", "End", "EndInEndTimeZone", _
		"EndTimeZone", "EndUTC", "EntryID", "ForceUpdateToAllAttendees", _
		"FormDescription", "GlobalAppointmentID", "Importance", "InternetCodepage", _
		"IsConflict", "IsRecurring", "LastModificationTime", "Location", _
		"MarkForDownload", "MeetingStatus", "MeetingWorkspaceURL", "MessageClass", _
		"Mileage", "NoAging", "OptionalAttendees", "Organizer", "OutlookInternalVersion", _
		"OutlookVersion", "RecurrenceState", "ReminderMinutesBeforeStart", _
        "ReminderOverrideDefault", "ReminderPlaySound", "ReminderSet", _
		"ReminderSoundFile", "ReplyTime", "RequiredAttendees", "Resources", _
		"ResponseRequested", "ResponseStatus", "Saved", "Sensitivity", "Size", _
		"Start", "StartTimeZone", "StartUTC", "Subject", "UnRead")
    
    Dim strLine As String, i As Integer
    strLine = ""
    For i = LBound(arrFields) To UBound(arrFields) 'traverse array with appointments fields
        strLine = strLine & arrFields(i) & ", " 'first line of CSV contains field names
    Next i
    strLine = Left(strLine, Len(strLine) - 2)
    Print #outFile, strLine ' CSV header
    
    Dim objItem As Object
    For Each objItem In objAppointmentsFolder.Items ' for each Outlook item 
        If (objItem.Class = olAppointment) Then     ' if it is an appointment
            strLine = ""
            With objItem
                For i = LBound(arrFields) To UBound(arrFields) 'assemble all fields of one appointment
                    strLine = strLine & Sanitize(CallByName(objItem, arrFields(i), VbGet)) & ", "
                Next i
            End With
            strLine = Left(strLine, Len(strLine) - 2)
            Print #outFile, strLine
        End If
    Next objItem
    Close #outFile
    Debug.Print "CalendarAppointments End " & Now

Exiting:
    Set objSession = Nothing
    Set objAppointmentsFolder = Nothing
    Exit Sub
ErrorHandler:
    Debug.Print Err.Number, Err.Description
    Resume Exiting
End Sub

Function Sanitize(ByVal str As String) As String
  ' get rid of single & double quotes, commas, carriage returns and semicolons, to maintain intact the CSV structure
  Sanitize = Replace(Replace(Replace(Replace(Replace(Replace(str, _
             "'", " "), """", " "), ",", ""), vbCrLf, " "), vbCr, " "), ";", " ")
End Function

On your Desktop you’ll find the CalendarAppointments.csv file that you can readily open with Excel to purview the entries fields and filter / sort as needed.

For the more inclined, I would suggest to import the CSV file into MS Access (First Row Contains Field Names; if truncation occurs for RequiredAttendees or OptionalAttendees, change Short Text -> Long Text). Then query extensively, like:

SELECT Year([Start]) AS StartYear, Month([Start]) AS StartMonth, 
       Count(EntryID) AS AppointmentCount
FROM CalendarAppointments
GROUP BY Year([Start]), Month([Start])
ORDER BY Count(EntryID) DESC;
BusiestMonthByAppointmentsCount
TRANSFORM Sum(Duration/60) AS DurationInHours
SELECT Month(Start) AS StartMonth
FROM CalendarAppointments
WHERE AllDayEvent="False" AND BusyStatus=2
GROUP BY Month(Start)
PIVOT Year(Start);
AppointmentDurationByMonth

Note: in the query above I included only accepted appointments (BusyStatus=2); I do not follow those that I mark as tentative and Outlook deletes those declined (in newer version that changes – at last). I also excluded full day events (usually holidays, etc.); each would contribute 24 hours to duration. Originally, duration is in minutes, so dividing by 60 gets it in hours. I pivoted by year and totals are actually average monthly durations for each year, so 2023 was busier than 2022.

Count emails in Outlook

In my former jobs, I spent some time trying to come up with productivity figures. Some years ago, I was working for a shipyard company and workers were assigned tasks on ship repairs. Their working time was recorded (entry – exit from the yard) to be used for their wages. Their supervisors also recorded the time on tasks carried out on the ships or, for example, when they were just standing by because there was no (productive) task to carry out. Taking into consideration hourly pay, insurance costs, indirect costs, etc, we could analytically (not statistically) compute the actual cost of the ship’s repair work, which was then subtracted from the invoiced amount to the ship-owner, leaving the net profit for the yard. Straightforward, eh?

How could someone do some analogous calculation (not estimation) for office work? How could an office worker find out whether he worked more or less for a specific month?

Nowadays, more and more office work is carried out by exchanging email messages. I thought, let’s experiment with this and see where it leads.

The basic idea

I would need to count the email messages exchanged daily. With those arriving in my Inbox, I need to spend some time to have an initial look at them. Some of them, I delete immediately. Some of them need my attention, so I need to carefully read them and, out of those, for some I need to carry out some work and reply back.

So, I need to count all incoming emails grouped by day, which is tedious to do, because at the end of the day, some of them are placed in the Deleted Emails and the rest are stacked in sub-folders.

Calendar appointments may also be of interest to you: instead on collecting email info, that collects calendar info.

I also need to count the Sent emails, because they require extra effort to be carried out and some time to write them.

So, I will use these two figures as KPIs for my job done. Other interesting info is further found, such as high/low volume time of day.

The implementation

A VBA script runs inside Outlook and collects all the necessary information about the emails, without me needing to count them.

The outputs of this scripts, which are CSV files, you can analyze them using some spreadsheet or database application to get useful results. I chose to use Microsoft Power BI, because it is easy to update its input datasets, it has some fancy visualizations, and offers handy slicing (filtering).

From time to time, you need to re-run the VBA script to get fresher mail counts for the most recent dates. In my case, the mail retention policy for the Deleted Items is 6 months, so after that period the older, deleted emails will disappear, therefore only the last 6 months mail counts are valid (in my case). After updating the CSV files, you Refresh the data sources in Power BI to see the statistics.

Step 1 Producing the data

At the end of this page, you will find The EmailCount VBA source code, which you can Copy as text. While in Outlook, you press Alt-F11 to open the Microsoft Visual Basic for Applications environment. You insert a new Module and inside you Paste the copied code.

Before running it, you need to change lines 17-18, where you should edit with your own email address.

Then place your cursor somewhere in the body of the EmailCount() subroutine and press Run (the green arrow in the menu).

This will create two files, prefixed by the current date, on your Desktop:

  • CountMails.csv gives the total count of emails, sent & received, for each day
  • LogMails.csv contains lines for each email found in all Outlook folders. Columns hold the datetime of the email, the folder the mail is currently placed in, 0 if it is an incoming email or -1 if it is a Sent Item.
The format of the CSV files

BTW, the VBA code also logs some informative facts in the Immediate window (press Ctrl-G to open) in the environment. It will also report if something goes wrong, so you’d better have a look at it, before proceeding with the data in the CSV files.

The contents of the log

Step 2 Getting the information

Ok, now that the raw data is in place, let’s deduce useful information out of them. I will sketch my approach using Power BI, but you can get similar outputs with Excel, Access, or whichever tool you are familiar with.

Step 2a Ingesting data

Files with Comma Separated Values are easily handled by any spreadsheet application. I fed them into Power BI promoting the first line in header and changing dates to the proper data type. I even added a computed column ReceivedHour = Format([ReceivedOn],"hh").

Data tables inside Power BI

Step 2b Reporting

For reporting you can select the data to drill into by year and month.

Year and month slicers for the tables

I created some charts, which may seem a bit dull, but the nice part is that they respond to slicing, justto get the idea.

November 2022 was by far the worst month.
You can clearly see the lunch break!

One can do nice things with the collected data. A next project I have in mind is to collect the appointments data from Outlook calendar and report the time spent in meetings. One can also combine email and calendar data to calculate interesting work KPIs.

Enjoy and let me know how much you torture your mail servers!

The EmailCount VBA source code

 

Option Explicit

' EmailCount() scans Outlook folders counting how many email messages were sent/received every day.
' The daily email count is then stored as a CSV file at your Desktop.
' Moreover, a log file contains for every message the sent timestamp, folder stored and if the message was sent.

' Version 2022-12-19 minor changes in Debug.Print
'         2022-12-05 running ok, producing CSV file at Desktop which is fed to Power BI for analysis.
'         2022-12-08 added second email


' When run at work, keep in mind that the detention policy for the Deleted folder is 183 days (6 months).

' To reset policy and allow the script to run, just delete file C:\Users\XXXusernameXXX\AppData\Roaming\Microsoft\Outlook\VbaProject.OTM
' and copy in a new module the script code. Otherwise, message "The macros in this project are disabled..." appears.

Const cstrMyEmail = "XXXgaviotisXXX@Xmail.com"  ' to find the emails that I sent
Const cstrMyEmail2 = "YYYgaviotisYYY@Ymail.com" ' an alternative email																											

Dim objDictionary As Object ' the data structure to store the collected data
Dim objLogFile As Object    ' the handle for the log file
    
Private Sub EmailCount()
On Error GoTo ErrorHandler

'traverses all mail in default mail account and outputs info in CSV files at desktop folder

    Dim objOutlookApplication As Object
    Dim objNameSpace As Object
    Debug.Print "EmailCount Start " & Now
    Set objOutlookApplication = CreateObject("Outlook.Application")
    Set objNameSpace = objOutlookApplication.GetNamespace("MAPI")
    
    Dim objFolder As Object
    ' Set objFolder = Application.ActiveExplorer.CurrentFolder ' This option allows to select a folder and count in it al all its subfolders'
    Set objFolder = objNameSpace.GetDefaultFolder(olFolderInbox).Parent ' This option gets all current mailbox folders
    
    Set objDictionary = CreateObject("Scripting.Dictionary") ' global variable to be accessible by the recursive routine
    
    Dim strLogFilename As String ' the filename where useful email data are logged
    strLogFilename = GetDesktop & "\" & cstrMyEmail & " " & GetDate(Now) & " LogMails.csv" ' You may change the log folder and filename here
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set objLogFile = fso.CreateTextFile(strLogFilename) ' global var to be accessible by the recursive routine
    objLogFile.WriteLine "ReceivedOn;InFolder;SentByMe" 'header of log file

    Call ProcessCurrentFolder(objFolder)
    
    Dim strOutputFilename As String
 
    strOutputFilename = GetDesktop & "\" & cstrMyEmail & " " & GetDate(Now) & " CountEmails.csv" ' You may change the output folder and filename here
    
    Call OutputResult(strOutputFilename)
    
    objLogFile.Close: Set fso = Nothing: Set objLogFile = Nothing ' log file garbage
    Set objDictionary = Nothing
    Set objFolder = Nothing
    Set objNameSpace = Nothing
    Set objOutlookApplication = Nothing
    Debug.Print "EmailCount End " & Now
    Exit Sub

ErrorHandler:
    Debug.Print Err.Number, Err.Description
    Resume Next
End Sub
 
Private Sub ProcessCurrentFolder(ByVal objFolder As Outlook.MAPIFolder)
On Error GoTo ErrorHandler

    ' process emails in the folder
    Dim intEmailCount As Integer
    intEmailCount = 0
    Debug.Print "Folder: " & objFolder & " Items: " & objFolder.Items.Count;
        
    ' When new items are created in the Contacts folder or the Recipient Cache folder (a hidden folder under the Contacts folder), related items are created in the PersonMetadata folder.
    If objFolder = "PersonMetadata" Then
        Debug.Print " SKIPPED" ' ignore this folder - it contains garbage anyways
        Exit Sub
    End If
        
    Dim objItem As Object, objItems As Outlook.Items
    Set objItems = objFolder.Items
    ' The SetColumns method is useful for iterating through an Items collection. If you don't use this method, Microsoft Outlook must open each item to access the property.
    ' With the SetColumns method, Outlook only checks the properties that you have cached, and provides fast, read-only access to these properties.
    ' objItems.SetColumns ("ReceivedTime")
    
    Dim strDate As String
    For Each objItem In objItems
        If TypeName(objItem) = "MailItem" Then
         intEmailCount = intEmailCount + 1
            strDate = GetDate(objItem.ReceivedTime)
            
            If Not objDictionary.Exists(strDate) Then
                objDictionary(strDate) = 0
            End If
            objDictionary(strDate) = CLng(objDictionary(strDate)) + 1
            
            objLogFile.WriteLine objItem.ReceivedTime & "; " & objFolder & "; " & _
                IIf(objItem.SenderEmailAddress = cstrMyEmail Or objItem.SenderEmailAddress = cstrMyEmail2, vbTrue, vbFalse)
        End If
    Next objItem
    Debug.Print " Mails:" & intEmailCount
    
    ' process subfolders in the folder recursively
    Dim objCurFolder As Outlook.MAPIFolder
    If (objFolder.Folders.Count > 0) Then
        For Each objCurFolder In objFolder.Folders
            Call ProcessCurrentFolder(objCurFolder)
        Next objCurFolder
    End If
    Exit Sub

ErrorHandler:
    Debug.Print Err.Number, Err.Description
    Resume Next
End Sub

Function OutputResult(ByVal strFileName As String) As String
    Dim strMessage As String
    Dim varKey As Variant
    
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    Dim oFile As Object
    Set oFile = fso.CreateTextFile(strFileName)
    
    oFile.WriteLine "ReceivedDate;EmailCount" 'header
    For Each varKey In objDictionary.Keys
        oFile.WriteLine varKey & "; " & objDictionary(varKey)
    Next
    
    oFile.Close
    Set fso = Nothing
    Set oFile = Nothing
End Function

Function GetDate(dt As Date) As String
    GetDate = Year(dt) & "-" & Format(Month(dt), "00") & "-" & Format(Day(dt), "00") 'just the date
End Function

Function GetDesktop() As String
    Dim oWSHShell As Object
    Set oWSHShell = CreateObject("WScript.Shell")
    GetDesktop = oWSHShell.SpecialFolders("Desktop")
    Set oWSHShell = Nothing
End Function