Handling COM event cancelation in VBScript - events

I would like to write a script, that sends an E-Mail via our companys SMTP-Server using CDO.
First I tried to write an HTA-application for that purpose, but it became rather fiddly to make it cormfortable enough so that other people may handle it well (because of proper recipient resolving).
So now I try to use the regular Outlook-Mail mask to prepare the mail first and then catch the send-item event via VBScript to give it's content to my CDO script.
Right now, my code looks like this:
Dim OutlookApplication
Dim MailItem
Const olDiscard = 1
Const olMailItem = 0
Set OutlookApplication = WScript.CreateObject("Outlook.Application", "Outlook_")
Set MailItem = OutlookApplication.CreateItem(olMailItem)
MailItem.Display
'(...) some code to add recipients, subject, text, etc... depending on the given WScript.Arguments
While Not MailItem Is Nothing
'keep the script alive
WScript.Sleep 1
WEnd
Function CDOSendMessage()
'some code to send the data to our smtp server, return true if successfull
CDOSendMessage = True
End Function
Sub Outlook_ItemSend(byVal Item, Cancel)
If Item.body = MailItem.body Then 'Any more fail proof suggestions on how to check if it's the correct mailitem I'm handling with this event? While the script is alive, it fires for EVERY mail I send via outlook
Cancel = True
If CDOSendMessage() then
Set MailItem = Nothing
MailItem.Close olDiscard
Else
Cancel = False
MsgBox "Sending message via CDO failed."
End If
End If
End Sub
The main problem is, that Cancel = True simply does not work. Outlook will send my mail using my regular mail adress no matter what. Can you tell me, what I'm doing wrong please?
Thank you very much in advance!
Guido

The Cancel parameter must be declared with the ByRef modifier.

Updated code as requested:
Dim OutlookApplication
Dim MailItem
Dim CDODone: CDODone = False
Const olDiscard = 1
Const olMailItem = 0
Set OutlookApplication = WScript.CreateObject("Outlook.Application", "Outlook_")
Set MailItem = OutlookApplication.CreateItem(olMailItem)
MailItem.UserProperties.Add "CDOFlag", 20, false, false
MailItem.Display
'(...) some code to add recipients, subject, text, etc... depending on the given WScript.Arguments
While Not CDODone Is Nothing
'keep the script alive
WScript.Sleep 1
WEnd
MailItem.Close olDiscard
Function CDOSendMessage()
'some code to send the data to our smtp server, return true if successfull
CDOSendMessage = True
End Function
Sub Outlook_ItemSend(byVal Item, byRef Cancel)
If Not Item.UserProperties.Find(CDOFlag) Is Nothing Then
Cancel = True
If CDOSendMessage() then
CDODOne = True
Else
Cancel = False
MsgBox "Sending message via CDO failed."
WScript.Quit
End If
End If
End Sub

Related

Outlook VBscript method .Send stops scripts

I am developing a script to send an email according to certain inputs, I am able to craft the email but not send it using the .Send method.
I am getting the following error: (please note that the line is matching the .Send use in the original case)
I have already successfully sent emails using the .SendKeys(^~) method, but I would like to be use Outlook object to do so and not simply send shortcuts.
This is my current code:
' Declare all variables that will be used later on
Dim outobj, mailobj, emailto, cc, subject, body, attachement
Dim strFileText
Dim objFileToRead
Dim splitEmailto
' Set the outlook application object
Set outobj = CreateObject("Outlook.Application")
' set the namespace
Set myNamespace = outobj.GetNameSpace("MAPI")
msgbox myNamespace.Folders(2)
' Set the mail item object
Set mailobj = outobj.CreateItem(olMailItem)
' Set a shell
Set WshShell = WScript.CreateObject("WScript.shell")
' Get all the argument and assign
emailto = "name#domain.eu"
cc = "name#domain.eu"
subject = "Simple Email"
body = "Some Text"
attachement = "C:\Users\name\Desktop\fileName.xls"
' Craft the email object
With mailobj
.Display
' assign the tos
.To = cstr(emailto)
' add CCs
.CC = cstr(cc)
' attach the relevant files
If attachement <> "" Then
If instr(attachement, ";") Then
splitAtt = split(attachement, ";")
For Each att In splitAtt
If att <> "" Then
.Attachments.add cstr(att)
End If
Next
Else
.Attachments.add cstr(attachement)
End If
End If
If Subject <> "" Then
.Subject = Subject ' sets the subject
End If
If body <> "" Then
.Body = body ' sets the body
End If
.Send
End With
' Clear the memory
Set outobj = Nothing
Set mailobj = Nothing
' check for no more events in the sending event
' Report out & Quits
WScript.StdOut.WriteLine("Email sent")
WScript.Quit
I would like to be able to send the email with the .Send. any idea?
The error is E_ABORT.
Why are you displaying the message and immediately calling Send? You either display the message (Display, but no Send), or just send it outwith displaying (Send, but no Display).

Quitting the IE in the end of VBA function

I implemented several functions which relies on downloading some information from some websites.
The simplest example of such a function is:
Public Function getSomething(webAddress As String)
Dim html As HTMLObjectElement
Set html = getWebContents(webAddress)
Set elems = html.body.getElementsByTagName(tagName)
...
End Function
The function for acquire data from websites is:
Public Function getWebContents(webAddress As String) As HTMLObjectElement
Dim ie As InternetExplorer
Dim html As HTMLDocument
Set ie = New InternetExplorer
ie.Visible = False
ie.Navigate webAddress
Do While ie.READYSTATE <> READYSTATE_COMPLETE
Application.StatusBar = "Trying ..."
DoEvents
Loop
Set getWebContents = ie.Document
'close down IE and reset status bar
'ie.Quit
Set ie = Nothing
Application.StatusBar = ""
End Function
The problem is that it seems that I need the line ie.Quit to be uncommented to close the IE instance. But when I uncomment ie.Quit the line
Set elems = html.body.getElementsByTagName(tagName)
generates errors.
It seems that I cannot use HTMLObjectElement returned by function getWebContents when IE has been quitted. How to deal with that? I could implement a try...finally block in getSomething function and open ie there and close in the finally block. However I have many functions of a similar nature and making many similar try...finally blocks seems a stupid idea.
Any thoughts?
Thanks!
You should define a procedure to handle the object lifetime from creation to destruction. You can then pass a reference for the object to the function.
Lastly, you can dispose the object even if an error occurs at any stange.
Public Sub Main()
On Error GoTo ErrProc
Dim ie As InternetExplorer
Set ie = New InternetExplorer
'....
Dim obj As Object
obj = getWebContents(ie, "url")
Leave:
ie.Quit
Set ie = Nothing
Set obj = Nothing
Application.StatusBar = ""
On Error GoTo 0
Exit Sub
ErrProc:
MsgBox Err.Description, vbCritical
Resume Leave
End Sub
Public Function getWebContents(ie As InternetExplorer, webAddress As String) As HTMLObjectElement
'...
End Function
You are keeping a pointer to the DOM in the html variable. If you close IE, you are pointing to something non-existing.
The simple answer is to close IE at the end of getSomething. In your case, this means that you have to restructure your code so that your IE variable is accessible from other places than in getWebContents

Why does my VBA script not continue after debugging?

Whenever I hit an error with my script, the focus turns to the VBA code and the offending line. I fix it, and hit save. Then I notice that the script is no longer running, even after I make sure that it's not paused.
For example, right now I'm using a Form_Timer() event to do some testing (interval set to 1000ms). To test the script again, I just set it to a minute in the future (e.g. if the current time is 8:54:00 AM I set it to fire at 8:55:00 AM). But this stops working after an error. Does anyone know why this is? I don't want to have to tell my users to close and re-open their copies of the Access DB just to make the script work again.
Code:
Private Sub Form_Timer()
On Error GoTo ErrorHandler
current_date_time = Now
If current_date_time = #6/28/2016 8:52:00 AM# Then
MsgBox ("the current_date_time variable holds: " & current_date_time)
'Declare objects
Dim dbs As DAO.Database
Dim rst As DAO.Recordset
Dim qdf As DAO.QueryDef
Dim oApp As Outlook.Application
Dim oMail As Outlook.MailItem
Dim mail_body As String
'Set objects
Set dbs = CurrentDb
Set qdf = dbs.QueryDefs("qry_BMBFLoc")
Set rst = qdf.OpenRecordset
Set oApp = New Outlook.Application
Set oMail = oApp.CreateItem(olMailItem)
mail_body = "The following jobs do not have the special BF location set in Job Orders: " & vbCrLf
If Not (rst.EOF And rst.BOF) Then
rst.MoveFirst
Do Until rst.EOF = True
mail_body = mail_body & rst!job & "-" & rst!suffix & vbCrLf
rst.MoveNext
Loop
'Email contents
oMail.Body = mail_body
oMail.Subject = "Blah"
oMail.To = "someone#something.com"
oMail.Send
'Close stuff
rst.Close
dbs.Close
Set rst = Nothing
Set oMail = Nothing
Set oApp = Nothing
End If
End If
Exit Sub
ErrorHandler:
Dim msg As String
If Err.Number <> 0 Then
msg = "email Form Timer Error #" & Str(Err.Number) & " error Line: " & Erl & Chr(13) & Err.Description
MsgBox msg, , "Error", Err.HelpFile, Err.HelpContext
End If
Exit Sub
End Sub
In order to reactivate the code, you could close the form when the error is triggered. The user would then have to reload the form to complete the action.
However, without any intervention the error is likely to occur again.
Edit: Or you could write a Function to automatically close, and re-open the offending form. Calling it in the on error command.
When there is an error in access form, the timer will stop working, you don't need to close and reopen the whole database, only the form to start the timer again. Otherwise you can add a button called "refresh" and bind macro to it which will turn the timer on again.
Yeah this sucks. I am writing a vba script for outlook and so the only way to debug is to close and reopen outlook after every error.

Quit Outlook object AFTER email is sent

Here is the code I have so far:
Option Explicit
Call OpenOutlook()
Function OpenOutlook()
Dim ObjShell
Set ObjShell = CreateObject("WScript.Shell")
ObjShell.Run("Outlook.exe")
Call SendEmail()
'I tried closing from here but this didn't work either
'ObjShell.Quit
End Function
Function SendEmail()
'Declaring variables used through out this function
Dim ObjOutlook
Dim objMail
Set ObjOutlook = CreateObject("Outlook.Application")
'CreateItem(0) opens a New Email window...MailItem
set objMail = ObjOutlook.CreateItem(0)
objMail.Display
'MailItem Options
objMail.to = "test#mail.com.com"
'objMail.cc = "test2#mail.com"
objMail.Subject = "Did it work!?"
objMail.Body = "If you got this email, my VBs test worked!"
'objMail.Attachments.Add("C:\Attachment\abc.jpg")
objMail.Send
'This didn't work either
'If objMail.Sent = True Then
'ObjOutlook.Quit
'End If
'Quit closes Outlook like I want but it doesn't wait for the email to send
'ObjOutlook.Quit
End Function
What I'm trying to automate using VBScript:
Open Outlook
Send an email
Wait for email to send (Outbox to finish sending)
Close Outlook AFTER the email has been sent
Where I'm stuck:
First of all, I was having trouble opening Outlook. Below is the code that I used to create an Outlook Object:
Set ObjOutlook = CreateObject("Outlook.Application")
'CreateItem(0) opens a New Email window...MailItem
set objMail = ObjOutlook.CreateItem(0)
objMail.Display
What I did (Not even sure if this is the right way to do it):
Set ObjShell = CreateObject("WScript.Shell")
ObjShell.Run("Outlook.exe")
Why can't I just do ObjShell.Quit after I call the SendEmail() Function? Using .Quit gives me an error.
I just want to close the Outlook application once the email has been sent and I can't figure out how.
MailItem has a Sent property that indicates when the message has been sent. Try this:
...
objMail.Send
Do Until objMail.Sent
WScript.Sleep 500
Loop
' Safe to close...
ObjOutlook.Quit
Try this:
Option Explicit
Sub SendMail()
Dim outobj, mailobj
Set outobj = CreateObject("Outlook.Application")
Set mailobj = outobj.CreateItem(0)
With mailobj
.To = "user#test.com"
.Subject = "Testmail"
.Body = "If you got this email, my VBs test worked!"
.Send
End With
'Clear the memory
Set outobj = Nothing
Set mailobj = Nothing
End Sub
SendMail()
Msgbox("Done")
Firstly, what if it was the user who opened Outlook? I don't think any user will appreciate your code closing Outlook. You might want to check that both Application.Explorers.Count and Application.Inspectors.Count are zero before closing Outlook. Newest version of Outlook will automatically exist if there are no open explorers or inspectors even if you hold a reference to an Outlook object - that was done to guard ageist misbehaving apps that leaked references. To prevent Outlook from closing, hodl a reference to an Explorer object t(even if you do not show it) - call Namespace.getDefaultFolder(olFolderInbox), then hold a reference to the object returned by MAPIFolder.GetExplorer .
Secondly, after calling Send, use Namespace,SyncObjects collection to retrieve the very first SyncObject. Hook up the SyncObject.SyncEnd event and call SyncObject.Start. When SyncEnd event fires, you will be all done. But I don't think you can work with events in VB script...

Macro in outlook to mark emails as read

I want to use a macro in outlook 2013. This macro is supposed to mark any emails arriving a specific folder ('work' folder) as read. I'm not familiar with vb. Any help/guidance is much appreciated!
No sure, I have heard this one before of wanting emails automatically read. You have two options:
a) Use Ctrl-A (select all mail in folder), Ctrl-Q (mark selection as read)
b) Use New Email Event something like:
Private Sub Application_NewMailEx(ByVal EntryIDCollection As String)
vID = Split(EntryIDCollection, ",")
Dim i as Long, objMail as Outlook.MailItem
For i = 0 To UBound(vID)
Set objMail = Application.Session.GetItemFromID(vID(i))
objMail.Unread = False
Next i
End Sub
Private Sub Application_NewMailEx(ByVal EntryIDCollection As String)
' version to select folder
Dim i As Long, objMail As Outlook.MailItem, mpfInbox As Outlook.Folder
Set mpfInbox = Application.GetNamespace("MAPI").Folders("YOURACCOUNT").Folders("[Gmail]").Folders("Sent Mail")
For i = 1 To mpfInbox.Items.Count
If mpfInbox.Items(i).Class = olMail Then
Set objMail = mpfInbox.Items.Item(i)
objMail.UnRead = False
End If
Next i
End Sub
You can set up a rule which can trigger your macro.
I'd not suggest working with the NewMailEx event because it is not fired in some case and may introduce issues. See Outlook NewMail event unleashed: the challenge (NewMail, NewMailEx, ItemAdd) for more information.

Resources