VBA – Retrieve Outlook Signature File

I’ve been doing some work with Outlook recently and seeing postings to read signature files that have hard codes file names

sSig = CreateObject("Scripting.FileSystemObject").GetFile(Environ("appdata") & "\Microsoft\Signatures\MySignatureFile.htm").OpenAsTextStream(1, -2).ReadAll

Now this is great and all, but that means you need to know the name of the file and update it for each user, update it should they ever create a new one … a nightmare to manage!

So I set out to find a better way!

The Nightmare That Is Outlook

I’m not going to go on and on about it, but needless to say, Microsoft sure did not make thing easy for us developers and documentation about certain aspects appears, at least to me, to be non-existent. So it took some serious investigative work to figure out which registry keys were involved, … I so wish that Microsoft would standardize and cleanup their object models and gives us some desperately needed methods and properties, but if they haven’t by now I doubt I’ll ever see it since now all their energies are on the cloud!

The fact that even with the object available to me, I still have to go through the registry to get information is just mind blowing in 2020!

Moving on now.

The Solution

The solution requires a few procedures:

  • Determine the version of Outlook the user has so we can look things up in the registry
  • Lookup values in the registry
  • Encode string as proper HTML
  • Read the signature

Long story short, this is what I came up with. The nice thing here is that I convert the relative paths used by Outlook into absolute paths so the signature HTML could be used by any e-mail client (Outlook, CDO, …).

'---------------------------------------------------------------------------------------
' Procedure : Outlook_GetSignature
' Author    : Daniel Pineault, CARDA Consultants Inc.
' Website   : http://www.cardaconsultants.com
' Purpose   : Finds and returns the signature (HTML) for a given account.
' Copyright : The following is release as Attribution-ShareAlike 4.0 International
'             (CC BY-SA 4.0) - https://creativecommons.org/licenses/by-sa/4.0/
' Req'd Refs: Late Binding version  -> None required
'
' Input Variables:
' ~~~~~~~~~~~~~~~~
' sAccount      : Optional, e-mail account to get the signature of, if not specified
'                   the default account signature will be retrieved
'
' Usage:
' ~~~~~~
' sSig = Outlook_GetSignature
' sSig = Outlook_GetSignature("john.doe@hotmail.com")
'
' Revision History:
' Rev       Date(yyyy/mm/dd)        Description
' **************************************************************************************
' 1         2020-12-08              Initial Release
'---------------------------------------------------------------------------------------
Function Outlook_GetSignature(Optional sAccount As String)
    Dim SignatueFileName      As String
    Dim SignatueFileName_Encoded As String
    Dim SignaturePath         As String
    Dim SignaturePath_Encoded As String
    Dim SignatueFile          As String
    Dim Signature             As String
    Dim SignatureFilePaths    As String
    Dim SignatureFilePaths_Encoded As String

    On Error GoTo Error_Handler

    If sAccount = "" Then
        SignatueFileName = Outlook_GetSignatureFileName
    Else
        SignatueFileName = Outlook_GetSignatureFileName(sAccount)
    End If

    'Get the path of the signature files
    SignaturePath = Environ("appdata") & "\Microsoft\Signatures\"
    'Get the path and filename of the signature file
    SignatueFile = SignaturePath & SignatueFileName & ".htm"
    'Read the actual signature file into memory
    Signature = CreateObject("Scripting.FileSystemObject").GetFile(SignatueFile).OpenAsTextStream(1, -2).ReadAll
    'Convert relative paths in signature to absolute ones
    SignatueFileName_Encoded = encodeURI(SignatueFileName)
    SignatureFilePaths_Encoded = SignatueFileName_Encoded & "_files/"
    SignaturePath_Encoded = Replace(SignaturePath, "\", "/")
    SignaturePath_Encoded = encodeURI(SignaturePath_Encoded)
    SignaturePath_Encoded = Replace(SignaturePath_Encoded, "%3A", ":")
    SignaturePath_Encoded = Replace(SignaturePath_Encoded, "%2F", "/")
    SignaturePath_Encoded = Replace(SignaturePath_Encoded, "%5C", "\")
    Signature = Replace(Signature, SignatureFilePaths_Encoded, "file://" & SignaturePath_Encoded & SignatureFilePaths_Encoded)
    'Return the final result
    Outlook_GetSignature = Signature

Error_Handler_Exit:
    On Error Resume Next
    Exit Function

Error_Handler:
    MsgBox "The following error has occured" & vbCrLf & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Source: Outlook_GetSignature" & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occured!"
End Function

'---------------------------------------------------------------------------------------
' Procedure : Outlook_GetSignatureFileName
' Author    : Daniel Pineault, CARDA Consultants Inc.
' Website   : http://www.cardaconsultants.com
' Purpose   : Finds and returns the name of the signature file for a given account.
' Copyright : The following is release as Attribution-ShareAlike 4.0 International
'             (CC BY-SA 4.0) - https://creativecommons.org/licenses/by-sa/4.0/
' Req'd Refs: Late Binding version  -> None required
'
' Input Variables:
' ~~~~~~~~~~~~~~~~
' sAccount      : Optional, e-mail account to get the signature of, if not specified
'                   the default account signature will be retrieved
' bDisplayError : Optional, whether or not to display error to the user
'
' Usage:
' ~~~~~~
' sSig = Outlook_GetSignatureFileName
'   -> main
' sSig = Outlook_GetSignatureFileName("john.doe@hotmail.com")
'   -> JohnDoe
'
' Revision History:
' Rev       Date(yyyy/mm/dd)        Description
' **************************************************************************************
' 1         2020-12-08              Initial Release
' 2         2020-12-16              Minor tweak in the numbers were formatted in the
'                                   the various loops
'---------------------------------------------------------------------------------------
Function Outlook_GetSignatureFileName(Optional ByVal sAccount As String, _
                                      Optional bDisplayError As Boolean = False) As String
    Dim oWshShell             As Object
    Dim aValues               As Variant
    Dim i                     As Long
    Dim j                     As Long
    Dim sValue                As String
    Dim sOutlookVersion        As String
    Dim sAccountNo            As String

    On Error GoTo Error_Handler

    sOutlookVersion = GetAppVersion("OUTLOOK.EXE")

    Set oWshShell = CreateObject("WScript.Shell")

    'Determine the default account
    If sAccount = "" Then
        aValues = oWshShell.RegRead("HKCU\Software\Microsoft\Office\" & _
                                    sOutlookVersion & _
                                    "\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\" & _
                                    "{ED475418-B0D6-11D2-8C3B-00104B2A6676}")
        sAccountNo = aValues(0)    'Default Account No used for new e-mails
    Else
        On Error Resume Next
        For j = 0 To 99    'crude way to handle this
            aValues = oWshShell.RegRead("HKCU\Software\Microsoft\Office\" & _
                                        sOutlookVersion & _
                                        "\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\" & _
                                        Format(j, "00000000") & "\" & "Email")
            If IsEmpty(aValues) = False Then
                sValue = ""
                For i = LBound(aValues) To UBound(aValues) Step 2
                    If aValues(i) <> 0 Then
                        sValue = sValue & Chr(aValues(i))
                    End If
                Next
                If sAccount = sValue Then
                    sAccountNo = j
                    Exit For
                End If
            End If
            DoEvents
        Next j
        On Error GoTo Error_Handler
    End If

    'Retrieve the signature file name for the account
    aValues = oWshShell.RegRead("HKCU\Software\Microsoft\Office\" & _
                                sOutlookVersion & _
                                "\Outlook\Profiles\Outlook\9375CFF0413111d3B88A00104B2A6676\" & _
                                Format(sAccountNo, "00000000") & "\" & "New Signature")
    sValue = ""
    For i = LBound(aValues) To UBound(aValues) Step 2
        If aValues(i) <> 0 Then
            sValue = sValue & Chr(aValues(i))
        End If
    Next

    Outlook_GetSignatureFileName = sValue

Error_Handler_Exit:
    On Error Resume Next
    If Not oWshShell Is Nothing Then Set oWshShell = Nothing
    Exit Function

Error_Handler:
    If Err.Number = -2147024894 Then    '***Registry Doesn't Exist
        If bDisplayError = True Then MsgBox "The requested registry key does not exist."
    Else
        MsgBox "The following error has occured" & vbCrLf & vbCrLf & _
               "Error Number: " & Err.Number & vbCrLf & _
               "Error Source: Outlook_GetSignatureFileName" & vbCrLf & _
               "Error Description: " & Err.Description & _
               Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
               , vbOKOnly + vbCritical, "An Error has Occured!"
    End If
    Resume Error_Handler_Exit
End Function

'---------------------------------------------------------------------------------------
' Procedure : GetAppVersion
' Author    : Daniel Pineault, CARDA Consultants Inc.
' Website   : http://www.cardaconsultants.com
' Purpose   : Return the version of the specified application
' Copyright : The following is release as Attribution-ShareAlike 4.0 International
'             (CC BY-SA 4.0) - https://creativecommons.org/licenses/by-sa/4.0/
' Req'd Refs: Late Binding version  -> None required
'
' Input Variables:
' ~~~~~~~~~~~~~~~~
' sAppExe       : the EXE filename of the application to lookup
'
' Usage:
' ~~~~~~
' Call GetAppVersion("outlook.exe")
'   -> 15.0
' Call GetAppVersion("msaccess.exe")
'   -> 16.0
'
' Revision History:
' Rev       Date(yyyy/mm/dd)        Description
' **************************************************************************************
' 1         2020-12-08              Initial Release
'---------------------------------------------------------------------------------------
Public Function GetAppVersion(ByVal sAppExe As String) As String
    Dim oRegistry             As Object
    Dim oWMI                  As Object
    Dim oItems                As Object
    Dim oItem                 As Object
    Dim sKey                  As String
    Dim sValue                As String
    Dim sAppVersion           As String
    Const HKEY_LOCAL_MACHINE = &H80000002

    On Error GoTo Error_Handler

    Set oRegistry = GetObject("winmgmts:{impersonationLevel=impersonate}//./root/default:StdRegProv")
    Set oWMI = GetObject("winmgmts:\\.\root\cimv2")
    sKey = "Software\Microsoft\Windows\CurrentVersion\App Paths"
    oRegistry.GetStringValue HKEY_LOCAL_MACHINE, sKey & "\" & sAppExe, "", sValue
    Set oItems = oWMI.ExecQuery _
                 ("Select * from CIM_Datafile Where Name = '" & Replace(sValue, "\", "\\") & "'")
    For Each oItem In oItems
        GetAppVersion = Split(oItem.Version, ".")(0) & "." & Split(oItem.Version, ".")(1)
    Next

Error_Handler_Exit:
    On Error Resume Next
    If Not oItem Is Nothing Then Set oItem = Nothing
    If Not oItems Is Nothing Then Set oItems = Nothing
    If Not oWMI Is Nothing Then Set oWMI = Nothing
    If Not oRegistry Is Nothing Then Set oRegistry = Nothing
    Exit Function

Error_Handler:
    MsgBox "The following error has occured" & vbCrLf & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Source: GetAppVersion" & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occured!"
    Resume Error_Handler_Exit
End Function

Public Function encodeURI(ByVal sURI As String) As String
    Dim oScriptControl        As Object

    On Error GoTo Error_Handler

    Set oScriptControl = CreateObject("ScriptControl")
    oScriptControl.Language = "JScript"
    encodeURI = oScriptControl.Run("encodeURIComponent", sURI)

Error_Handler_Exit:
    On Error Resume Next
    If Not oScriptControl Is Nothing Then Set oScriptControl = Nothing
    Exit Function

Error_Handler:
    MsgBox "The following error has occured" & vbCrLf & vbCrLf & _
           "Error Number: " & Err.Number & vbCrLf & _
           "Error Source: encodeURI" & vbCrLf & _
           "Error Description: " & Err.Description & _
           Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
           , vbOKOnly + vbCritical, "An Error has Occured!"
End Function

and now with this code, with 1 line, we can retrieve the html of any signature.

For the default e-mail account you would simply do:

sSig = Outlook_GetSignature

Or, for a specific account, you would do:

sSig = Outlook_GetSignature("john.doe@hotmail.com")

2 responses on “VBA – Retrieve Outlook Signature File

  1. patrick

    nice app, but where i have to put the line sSig = Outlook_GetSignature ?

    for now if i start a new mail, i get a blank body , if i put a body text inside and change the from it deletes the body and also a blank body scrreen. thanks for this

    1. Daniel Pineault Post author

      Say you have an Outlook function like:

      Function SendHTMLEmail(ByVal sTo As String, _
                             ByVal sSubject As String, _
                             ByVal sBody As String, _
                             ByVal bEdit As Boolean, _
                             Optional vCC As Variant, _
                             Optional vBCC As Variant)
          On Error GoTo Error_Handler
          '    #Const EarlyBind = 1 'Use Early Binding
          #Const EarlyBind = 0    'Use Late Binding
          #If EarlyBind Then
              Dim oOutlook          As Outlook.Application
              Dim oOutlookMsg       As Outlook.MailItem
              Dim oOutlookInsp      As Outlook.Inspector
              Dim oOutlookRecip     As Outlook.Recipient
          #Else
              Dim oOutlook          As Object
              Dim oOutlookMsg       As Object
              Dim oOutlookInsp      As Object
              Dim oOutlookRecip     As Object
              Const olMailItem = 0
              Const olFormatHTML = 2
          #End If
          Dim sHTML                 As String
          Dim aHTML                 As Variant
          Dim aSubHTML              As Variant
          Dim aRecip                As Variant
          Dim i                     As Integer
       
          Set oOutlook = CreateObject("Outlook.Application")
          Set oOutlookMsg = oOutlook.CreateItem(olMailItem)
       
          With oOutlookMsg
              .Display    'Had to move this command here to resolve a bug only existent in Access 2016!
       
              'TO
              aRecip = Split(sTo, ";")
              For i = 0 To UBound(aRecip)
                  If Trim(aRecip(i) & "") <> "" Then
                      Set oOutlookRecip = .Recipients.Add(aRecip(i))
                      oOutlookRecip.Type = 1
                  End If
              Next i
       
              'CC
              If Not IsMissing(vCC) Then
                  aRecip = Split(vCC, ";")
                  For i = 0 To UBound(aRecip)
                      If Trim(aRecip(i) & "") <> "" Then
                          Set oOutlookRecip = .Recipients.Add(aRecip(i))
                          oOutlookRecip.Type = 2
                      End If
                  Next i
              End If
       
              'BCC
              If Not IsMissing(vBCC) Then
                  aRecip = Split(vBCC, ";")
                  For i = 0 To UBound(aRecip)
                      If Trim(aRecip(i) & "") <> "" Then
                          Set oOutlookRecip = .Recipients.Add(aRecip(i))
                          oOutlookRecip.Type = 3
                      End If
                  Next i
              End If
       
              .Subject = sSubject
              .HTMLBody = sBody
              .BodyFormat = olFormatHTML
              .Importance = 1    'Importance Level  0=Low,1=Normal,2=High
       
              For Each oOutlookRecip In .Recipients
                  If Not oOutlookRecip.Resolve Then
                      'You may wish to make this a MsgBox! to show the user that there is a problem
                      Debug.Print "Could not resolve the e-mail address: ", oOutlookRecip.Name, oOutlookRecip.Address, _
                                  Switch(oOutlookRecip.Type = 1, "TO", _
                                         oOutlookRecip.Type = 2, "CC", _
                                         oOutlookRecip.Type = 3, "BCC")
                      bEdit = True    'Problem so let display the message to the user so they can address it.
                  End If
              Next
       
              If bEdit = True Then    'Choose btw transparent/silent send and preview send
                  '.Display 'Preview
              Else
                  .Send    'Automatically send the e-mail w/o user intervention
              End If
          End With
       
      Error_Handler_Exit:
          On Error Resume Next
          If Not oOutlookRecip Is Nothing Then Set oOutlookRecip = Nothing
          If Not oOutlookInsp Is Nothing Then Set oOutlookInsp = Nothing
          If Not oOutlookMsg Is Nothing Then Set oOutlookMsg = Nothing
          If Not oOutlook Is Nothing Then Set oOutlook = Nothing
          Exit Function
       
      Error_Handler:
          If Err.Number = "287" Then
              MsgBox "You clicked No to the Outlook security warning. " & _
                     "Rerun the procedure and click Yes to access e-mail " & _
                     "addresses to send your message. For more information, " & _
                     "see the document at http://www.microsoft.com/office" & _
                     "/previous/outlook/downloads/security.asp."
          Else
              MsgBox "The following error has occurred" & vbCrLf & vbCrLf & _
                     "Error Number: " & Err.Number & vbCrLf & _
                     "Error Source: SendHTMLEmail" & vbCrLf & _
                     "Error Description: " & Err.Description & _
                     Switch(Erl = 0, "", Erl <> 0, vbCrLf & "Line No: " & Erl) _
                     , vbOKOnly + vbCritical, "An Error has Occurred!"
          End If
          Resume Error_Handler_Exit
      End Function

      Then you could use it by doing:

      Sub TestSig()
          Dim sSig                  As String
          Dim sMsg                  As String
      
          sSig = Outlook_GetSignature
          sMsg = "Just Testing Outlook Automation." & sSig
          Call SendHTMLEmail("someone@somewhere.com", "My Subject", sMsg, True)
      End Sub

      and the same is true if you are automation another e-mail client, using CDO, …. You build you message and concatenate the signature from this function.

      That said, if you are doing Outlook automation and not trying to automate using Thunderbird, CDO, …, you may prefer using: https://www.devhut.net/vba-send-html-emails-using-outlook-automation-improved/.