Determine if Outlook is available for automation - vba

I have a module that will automate Outlook but it should be skipped if Outlook isn't available.
Simply checking whether Outlook is installed is not sufficient because if there is a fresh Office install, launching Outlook will simply launch the configuration wizard. From my POV, Outlook is not available for automation so the module shouldn't be used even though it might be installed.
From my tests and the suggestions in this question, I can successfully trap for whether Outlook isn't configured yet but there is an edge case where this fails. This is when there is a dialog that asks to select a profile. In this situation, the check returns true but Outlook is actually not usable for the purposes of automation due to still needing additional configuration (e.g. selecting a profile). Is it possible to also trap this edge case?
To reproduce the "Select Profile" issue, go to Control Panel -> Mail. On the dialog, there is a option to "When starting Microsoft Outlook, use this profile" - select "Prompt for a profile used". When you then launch Outlook, you are asked to choose a profile. That is the case when the code below will fail.
This is my almost-working code so far...
Public Function DetectOutlookProfile() As Boolean
Dim objOutlook As Object
Dim objReg As Object
Dim varSplit As Variant
Dim lngMajor As Long
Dim strPath As String
Dim varSubKeys As Variant
Dim varSubKey As Variant
Const HKEY_CURRENT_USER As Long = &H80000001
On Error GoTo ErrHandler
Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
'Get an instance of Outlook so that we can determine the version
'being currently used by the current user.
Set objOutlook = CreateObject("Outlook.Application")
varSplit = Split(objOutlook.Version, ".")
lngMajor = varSplit(0)
If lngMajor <= 14 Then
'Outlook profile isn't version specific for Outlook 97-2010
strPath = "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles"
Else
'Outlook profile is version specific for Outlook 2013+
strPath = "Software\Microsoft\Office\" & lngMajor & ".0\Outlook\Profiles"
End If
objReg.EnumKey HKEY_CURRENT_USER, strPath, varSubKeys
For Each varSubKey In varSubKeys
DetectOutlookProfile = True
Exit For
Next
ExitProc:
On Error Resume Next
Exit Function
ErrHandler:
'Silently fail and return false
Select Case Err.Number
Case Else
DetectOutlookProfile = False
Debug.Print Err.Number & " (" & Err.Description & ")"
End Select
Resume ExitProc
Resume
End Function

Thanks to #David Zemens' suggestions, I found a solution that seems to work.
It seems that I don't even need to bother with registry checks. I can simply do this instead:
Set objOutlook = CreateObject("Outlook.Application")
DetectOutlookProfile = Len(objOutlook.GetNamespace("MAPI").CurrentProfileName)
Which will return 0 whether the Outlook has no profiles or is requiring a manual profile selection.
I suppose the registry check is needed to determine whether the Outlook has any profiles configured so that one's code could be then written to manually prompt the user for profile to be passed into Login method. For my case, I just don't want to run the module in either case, so the checking Len() of the current profile name suffices.

Related

GetObject doesn't return a reference to Outlook if just started

This code is from a larger block (a VBA Class running in MS Access).
Public Sub mrm_StartOutlook()
Dim m_objOutlook As Outlook.Application
Dim m_objNamespace As Outlook.NameSpace
140 On Error GoTo PROC_ERR
150 Set m_objOutlook = GetObject(, "Outlook.Application") 'reliably fails if Outlook just opened.
'Set m_objOutlook = CreateObject("Outlook.Application") 'reliably works
160 Set m_objNamespace = m_objOutlook.GetNamespace("MAPI")
170 MsgBox m_objOutlook.Name & ", " & m_objNamespace.Accounts.Item(1)
PROC_EXIT:
180 Exit Sub
PROC_ERR:
190 MsgBox "line " & Erl & " Error: " & Err.Number & ". " & Err.Description, , _
"StartOutlook"
200 Resume PROC_EXIT
End Sub
If Outlook has just started, line 150, using GetObject, triggers Error 429.
If I start Outlook manually, switch to the code window, wait for about 10 seconds, then call the sub above, it runs fine.
If I let Outlook open so that it's got the focus I can run the sub after about 5 seconds.
CreateObject always works.
This matters in a situation where I've checked if Outlook is open, found it isn't, opened it programmatically, and then run the sub.
It also matters where a user opens Outlook manually and then quickly runs my code.
I'm aware that any object variable pointing to Outlook is pointing to the same instance, but it would be good to get this to work reliably.
I suspect Outlook is not properly initialized, maybe the MAPI connection is not established. Is there a MAPI property that means - "ready and waiting" or similar?
I tried using timer pauses to give Outlook a chance to start properly. That seems clumsy. I either have to set the timer too long, and waste users' time, or too short and risk error 429.
You are better off always using CreateObject - as you mentioned, since Outlook is a singleton, you'd always get the same object.
If you want to check whether Outlook was running before, check if Application.Explorers.Count and Application.Inspectors.Count are both zero.

Outlook VBA fails to save changes to MailItem on older items only

I have a macro in Outlook VBA that is designed to set the category of the selected email and send a reply email to the sender when a user clicks a button on the "Ribbon".
The below code will properly set the category on any newer emails, but throws "Run-time error '440': Cannot save this item." when run on emails that have been sitting around for a while. I don't know what causes this, as it runs fine for a while and then no longer works.
I tried restarting Outlook, logging in and logging out, and restarting the computer, and none of these seem to trigger the change in behavior, so I believe it is somehow related to the duration that the email has been sitting. This is rather confusing, as I don't see why length of time sitting in the folder should affect the ability to save, but not the ability to access the properties of the MailItem.
A simplified version of the code (without the email-sending part, which works fine) is as follows:
Public Sub UpdateCategory
Dim objItem as Object
Set objItem = GetCurrentItem()
'Verify that selected item is an email and an engineering request
If TypeName(objItem) = "MailItem" And InStr(LCase(objItem.Subject), "engineering request") > 0 Then
objItem.Categories = "Test"
objItem.Save
End If
Set objItem = Nothing
End Sub
Function GetCurrentItem() As Object
Dim objApp As Outlook.Application
Set objApp = Application
On Error Resume Next
Select Case TypeName(objApp.ActiveWindow)
Case "Explorer"
Set GetCurrentItem = objApp.ActiveExplorer.Selection.Item(1)
Case "Inspector"
Set GetCurrentItem = objApp.ActiveInspector.CurrentItem
End Select
Set objApp = Nothing
End Function
Any thoughts or suggestions would be greatly appreciated!!
Is this is an Exchange mailbox, it is possible that Exchange modified the item on the server side, that change was downloaded to the OST file, but since Outlook Object Model is not aware of the change, when you try to save the (stale) item, you get a conflict error.

Save Attachments From New Email

I'm trying to use Outlook VBA to check all my emails on startup, and whenever I receive a new email, to see if the email subject is "Sample Daily Data Pull". If the email subject matches, I want outlook to save the attachment to a specified network drive folder. Here is the code I have:
In "ThisOutlookSession"
Option Explicit
Private WithEvents inboxItems As Outlook.Items
Private Sub Application_Startup()
Dim outlookApp As Outlook.Application
Dim objectNS As Outlook.NameSpace
Set outlookApp = Outlook.Application
Set objectNS = outlookApp.GetNamespace("MAPI")
Set inboxItems = objectNS.GetDefaultFolder(olFolderInbox).Items
End Sub
Private Sub inboxItems_ItemAdd(ByVal Item As Object)
Dim Msg As Outlook.MailItem
If TypeName(Item) = "MailItem" Then
If Item.Subject = "Sample Daily Data Pull" Then
Call SaveAttachmentsToDisk
Else
End If
End If
End Sub
I also have the following code in a module:
Public Sub SaveAttachmentsToDisk(MItem As Outlook.MailItem)
Dim oAttachment As Outlook.Attachment
Dim sSaveFolder As String
sSaveFolder = "N:\SampleFilePath\"
For Each oAttachment In MItem.Attachments
oAttachment.SaveAsFile sSaveFolder & oAttachment.DisplayName
Next
End Sub
This is my first time working in Outlook VBA, so my apologies if it's something very basic and obvious. Not really sure what is going wrong as I'm not getting any error messages. All I know is that the the macro is not saving attachments on my network drive as it should be.
Thanks in advance for any help.
Your code does not work for me because of:
Set inboxItems = objectNS.GetDefaultFolder(olFolderInbox).Items
Outlook saves mail items, calendar items, tasks and other such information in files it calls Stores. You can have several stores each of which will have an Inbox. I am a home user with two email accounts. I did a default installation of Outlook then used a wizard to add an account for each of my email addresses. The result is I had three stores:
Outlook Data File
MyName#myisp.com
MyName#gmail.com
“Outlook Data File” is the default store and contains the default Inbox but new emails are placed in the Inboxes in the other two stores. To test if you have the same problem, open Outlook, open the VBA Editor, type the following into your Immediate Window and press [Return].
? Session.GetDefaultFolder(olFolderInbox).Parent.Name
On my system, this statement outputs “Outlook Data File” because that store contains the default Inbox. If I want to have an event handler for new emails I need to have:
Private Sub Application_Startup()
Set InboxItems = Session.Folders("MyName#myisp.com").Folders("Inbox").Items
End Sub
This is someone shorter than your macro, which I will explain later, but the key difference is I am naming the Inbox I wish to monitor. If the Inbox that receives your new emails is not Outlook’s default Inbox, you will have to name the folder containing the Inbox you wish to monitor.
Why is my macro so much shorter than yours?
Dim outlookApp As Outlook.Application
Set outlookApp = Outlook.Application
You are already within Outlook so these statements are redundant.
You could replace:
Set objectNS = outlookApp.GetNamespace("MAPI")
by
Set objectNS = Application.GetNamespace("MAPI")
But you do not have to. The only GetNamespace is under Application so the qualification is optional. The only qualification that I know to be non-optional is Outlook.Folder and Scripting.Folder. If you write Folder within Outlook it assumes you want one of its folders. If you want to refer to a disk folder you must say so.
You have:
Dim objectNS As Outlook.NameSpace
Set objectNS = outlookApp.GetNamespace("MAPI")
I have used Session. The documentation states that Namespace and Session are identical. I prefer Session but most people seem to prefer Namespace. Your choice.
If you are references the correct Inbox, we need to look further for the cause of your problem.
The next possible issue is If Item.Subject = "Sample Daily Data Pull". This requires Item.Subject be exactly equal to "Sample Daily Data Pull". An extra space or a lower case letter and they are not equal.
Next, I suggest adding a statement at the top of each of procedure to give:
Private Sub Application_Startup()
Debug.Assert False
: : :
Private Sub inboxItems_ItemAdd(ByVal Item As Object)
Debug.Assert False
: : :
Public Sub SaveAttachmentsToDisk(MItem As Outlook.MailItem)
Debug.Assert False
: : :
Many programming languages have an Assertion statement; this is VBA’s version. It allows the programmer to assert that something will be true. Execution will stop if the assertion is false. I find Debug.Assert False invaluable during testing. Debug.Assert False will always be false so execution will always stop. This is an easy way to test that Application_Startup, inboxItems_ItemAdd and SaveAttachmentsToDisk are being executed.
Try the above suggestions. If they fail to find a problem, we will have to try something else.
Error Handling
In your original posting, you had:
On Error GoTo ErrorHandler
: : :
: : :
ExitNewItem:
Exit Sub
ErrorHandler:
MsgBox Err.Number & " - " & Err.Description
You will often see code like this but I have never seen a justification for it.
If an error occurs during development, this code will result in the error number and description being displayed and the routine exited. How is this helpful? It leaves you to guess from the error description which statement failed. If you omit all the error code, execution stops on the faulty statement. There is no guessing as to which statement was in error. If you can fix the error, you can click F5 and restart with the previously faulty statement. Even if you cannot fix and restart, you have a better understanding of the situation.
For a live system, I have difficulty in imagining anything less user friendly than an error resulting in display of a cryptic error message and the macro terminating.
For a live system, you want something like:
Dim ErrNum As Long
Dim ErrDesc As String
On Error Resume Next
Statement that might fail
ErrNum = Err.Num
ErrDesc = Err.Description
On Error GoTo 0
If ErrNum > 0 Then
' For each possible value for ErrNum, code to provide user friendly
' description of what has gone wrong and how to fix it.
End If
VBA is not the ideal language for writing code that fails gracefully but with care you can create some very acceptable error handling code.

Excel-VBA show Warning if Addon is not Installed

I am using an Excel 2013 file, which was updated by me to use PowerQuery for easier Data imports.
It already uses VBA Macros and i would like to include a Warning/MsgBox with a link to download PowerQuery, if it is not already installed.
How would i check for the existence of PowerQuery on the host System?
Adapting the code by Rory at the link i provided you would have something like the following. Note: You could use Rory's additional code to handle 2016 version or earlier ensuring if present is installed.
As you can't use a hyperlink direct i have adapted the Wiktor Stribiżew's code here that allows the user to click OK to go to the download site after getting msgbox saying not installed.
Option Explicit
Private Sub IsPowerQueryAvailable()
Dim downloadlink As String
downloadlink = "https://www.microsoft.com/en-gb/download/details.aspx?id=39379"
Dim bAvailable As Boolean
If Application.Version >= 16 Then
bAvailable = True
Else
On Error Resume Next
bAvailable = Application.COMAddIns("Microsoft.Mashup.Client.Excel").Connect
On Error GoTo 0
If Not bAvailable Then DownloadPowerQuery downloadlink
End If
End Sub
Private Sub DownloadPowerQuery(downloadlink As String)
Dim objShell As Object
Dim Message As String
Dim Wscript As Object
Set objShell = CreateObject("Wscript.Shell")
Message = MsgBox("Would you like to download PowerQuery?", vbYesNo, "Powerquery not available")
If Message = vbYes Then
objShell.Run (downloadlink)
Else
Wscript.Quit
End If
End Sub

VBScript to create an outlook rule

I'm trying to create a vbscript that I can distribute so an outlook rule is created for each user that runs it.
I have some code (below), however I have since found I can't create a rule via VBS with Actions.Run ("VBA Code"). I need a rule so that whenever an email is received from "test#test.com" a msgbox is displayed that the user must click OK an.
Through my research it indicates that the VBA may somehow be able to be implemented in the VBS file, but I can't find much on it.
The VBA I want to run is:
Sub newmsg(item As Outlook.MailItem)
MsgBox "You have an urgent message: " & item.Subject
End Sub
and the VBS is:
'--> Create some constants
Const RULE_NAME = "Urgent Message" '<-- Edit the name of the rule
Const olRuleReceive = 0
'--> Create some variables
Dim olkApp, olkSes, olkCol, olkRul, olkCon, olkAct
'--> Connect to Outlook
Set olkApp = CreateObject("Outlook.Application")
Set olkSes = olkApp.GetNamespace("MAPI")
olkSes.Logon olkApp.DefaultProfileName
'--> Get the rules collection
Set olkCol = olkSes.DefaultStore.GetRules()
'--> Create a new receive rule
Set olkRul = olkCol.Create(RULE_NAME, olRuleReceive)
'--> Set the rule's condition to look for a specific word in the subject
Set olkCon = olkRul.Conditions.From
With olkCon
.Enabled = True
.Recipients.Add ("email address here")
.Recipients.ResolveAll
End With
'--> Set the rule's action
Set olkAct = olkRul.Actions.Run("Project1.newmsg")
With olkAct
.Enabled = True
End With
'--> Save the rule
olkCol.Save False
'--> Disconnect from Outlook
olkSes.Logoff
Set olkCon = Nothing
Set olkAct = Nothing
Set olkRul = Nothing
Set olkCol = Nothing
Set olkSes = Nothing
Set olkApp = Nothing
'--> Terminate the script
WScript.Quit
The only supported way to modify VBA projects is by developing an add-in for the VBA editor using the Visual Basic Extensibility interfaces.
If you need to create a rule that performs a custom action then I recommend you build an Outlook add-in that processes incoming email messages and does the action within your add-in's code, rather than relying on a VBA method that may or may not exist.