I am trying to address a list of clients by their name when I send them meeting invitations. Clients must not see other invitees. I have tried several approaches: add clients one-by-one as a Resource, changing meeting notes text each time, forwarding the meeting as an iCalendar item, no luck.
Objective:
Simulate the Forward behavior of an Outlook Meeting.
Change all attendees from Required to Resource(I can do this)
I have done a lot of research and could not find a way to forward a meeting that simulates the user interface version.
Background information:
I have created a Zoom meetings appointment of which I know the location URL
I can successfully access this appointment as an Outlook.AppointmentItem using Restrict
I cannot add the list of clients directly as Resource because then I cannot customize each invite
I cannot use AppointmentItem.ForwardAsVcal as that forwards the meeting as an attachment and does not occupy calendar space for the client (also I believe it looks unprofessional)
I have failed to use MeetingItem.Forward because my object is an Outlook.AppointmentItem
I have successfully added new clients using Recipients.Add and .Type = olResource
I have successfully modified meeting notes using AppointmentItem.GetInspector().WordEditor.Range.FormattedText but this causes previous invites to be canceled and updates text in the invitation so everyone sees the last invite
Code:
Accessing the item successfully
Private Function getMeeting() As Outlook.AppointmentItem
Dim settingsWS As Worksheet
Set settingsWS = ThisWorkbook.Sheets("Settings")
Dim meetingStart As Date, meetingEnd As Date
meetingStart = settingsWS.Cells(2, 1).Value 'start time
Dim locationString As String
locationString = settingsWS.Cells(2, 2).Value 'location url
Dim oCalendar As Outlook.Folder
Dim oItems As Outlook.Items
Dim strRestriction As String
daStart = Format(meetingStart, "mm/dd/yyyy hh:mm AMPM")
daEnd = DateAdd("h", 2, daStart)
daEnd = Format(daEnd, "mm/dd/yyyy hh:mm AMPM")
strRestriction = "[Start] >= '" & daStart & "' AND [End] <= '" & daEnd & "'"
strRestriction = strRestriction & " AND [Location] = '" & locationString & "'"
Set oCalendar = GetNamespace("MAPI").GetDefaultFolder(olFolderCalendar)
Set oItems = oCalendar.Items.Restrict(strRestriction)
Set getMeeting = oItems(1)
End Function
My failed forwarding trials:
Private Sub sendInvites(oAppt As Outlook.AppointmentItem)
Dim oMail As Outlook.MailItem, oAtt As Outlook.Recipient, embeddedInvitation As OLEObject
Dim industryWS As Worksheet
Set industryWS = ThisWorkbook.ActiveSheet
Dim attendeeRange As Range
Set attendeeRange = industryWS.Cells(3, 1).CurrentRegion 'list of clients
Dim attendeeCompany As String, attendeeEmail As String
Dim attendeeName As String, attendeePrefix As String
Dim attendeeCount As Long, attendeeIndex As Long
attendeeCount = attendeeRange.Rows.Count - 1
For attendeeIndex = 1 To attendeeCount
attendeeCompany = attendeeRange.Cells(attendeeIndex + 1, 1).Value
attendeeEmail = attendeeRange.Cells(attendeeIndex + 1, 2).Value
attendeeName = attendeeRange.Cells(attendeeIndex + 1, 4).Value
attendeePrefix = attendeeRange.Cells(attendeeIndex + 1, 5).Value
Application.StatusBar = "Sending invites (" & CStr(attendeeIndex) & "/" & CStr(attendeeCount) & ") " & attendeeEmail
Set oMail = Outlook.Application.CreateItem(olMailItem)
'Set oMail = oAppt.ForwardAsVcal
oMail.To = attendeeEmail
oMail.BodyFormat = olFormatHTML
oMail.HTMLBody = getInvitationBody(attendeeName, attendeePrefix) & oMail.HTMLBody 'return invitation mailbody as HTML
'Dim fsd As MeetingItem
'fsd.Forward
'Set oAtt = oAppt.Recipients.Add(attendeeEmail)
'oAtt.Type = olResource
'oAppt.GetInspector().WordEditor.Range.FormattedText.Delete
'oMail.GetInspector().WordEditor.Range.FormattedText.Copy
'oAppt.GetInspector().WordEditor.Range.FormattedText.Paste
'oMail.Close False
'oAppt.ForwardAsVcal
'oAppt.Display
'oAppt.Send
oMail.Send
Application.StatusBar = "Saving invites (" & CStr(attendeeIndex) & "/" & CStr(attendeeCount) & ") " & attendeeEmail
'saveInvite oAppt, industryWS, attendeeRange
DoEvents
Next attendeeIndex
End Sub
I solved it after hours of trying.
My initial instinct was to loop through items in the olFolderCalendar since in the UI we access the meeting forward through the calendar, but objects in the calendar are Outlook.AppointmentItem rather than Outlook.MeetingItem which can be forwarded.
The solution is to send the meeting to yourself (or anyone in your organization) so that a copy of the meeting invitation is in your olFolderSentMail. Items in the olFolderSentMail are Outlook.MeetingItem, which can be forwarded. Unless you send it to someone thou, the meeting will not enter your olFolderSentMail.
We can filter our olFolderSentMail by using Restrict and location (URL) of the meeting. Once we have the Outlook.MeetingItem we can create a new Outlook.MeetingItem by calling MeetingItem.Forward on our existing meeting in our olFolderSentMail. Once we have the new Outlook.MeetingItem we can add clients to it as olResource. This will send a customized, otherwise invisible invitation to the client without notifying other clients.
One point of warning before I end this answer: My first approach was to loop in reverse through my olFolderSentMail so as not to loop over to many items and to save time, however keep in mind that everytime you forward the meeting, the new invitation ends up at the top of the olFolderSentMail, so if in reverse, you will be forwarding the forwarded invitation. When using the Restrict approach, you can simply use the first item, which should be your original invitation to yourself.
Related
I am trying to extract future calendar events from Outlook using the Items.Restrict method.
If the filter is not applied, it returns more than 70 results, older and future events.
When the filter for future events is applied, it returns around 20 results, most of them, future events, but also some old ones.
The Restrict filter is partially working, but I cannot understand why is not filtering those few old events.
Dim oOutlook As Object
Dim oMAPI As Object
Dim oAppointments As Object
Dim oFilteredAppointments As Object
Dim oAppointmentItem As Object
Dim sFilter As String
Const olFolderCalendar = 9
Set oOutlook = GetObject(, "Outlook.Application")
Set oMAPI = oOutlook.GetNamespace("MAPI")
Set oAppointments = oMAPI.GetDefaultFolder(olFolderCalendar)
sFilter = "[Start]>'" & Date & "'"
Debug.Print sFilter
Set oFilteredAppointments = oAppointments.Items.Restrict(sFilter)
For Each oAppointmentItem In oFilteredAppointments
Debug.Print oAppointmentItem.Start
Next
To show some evidence of the filter and the results I'm getting:
Calendars are trickier than normal folders. I had to combine the two filters as was suggested in Restrict Outlook Items by Date.
Note: oAppointmentItems rather than oAppointments.Items.
Option Explicit
Private Sub calApptsInSpecifiedRange()
Dim oCalendarFolder As Folder
Dim oAppointmentItems As Items
Dim oFilteredAppointments As Items
Dim oAppointmentItem As Object
Dim sFilter As String
Set oCalendarFolder = Session.GetDefaultFolder(olFolderCalendar)
Set oAppointmentItems = oCalendarFolder.Items
oAppointmentItems.Sort "[Start]", False
oAppointmentItems.IncludeRecurrences = True
sFilter = "[Start] > '" & Date & "'" & " AND [Start] < " & "'" & Date + 30 & "'"
Debug.Print sFilter
Set oFilteredAppointments = oAppointmentItems.Restrict(sFilter)
For Each oAppointmentItem In oFilteredAppointments
Debug.Print oAppointmentItem.Start, oAppointmentItem.Subject
Next
Debug.Print "Done."
End Sub
To retrieve all Outlook appointment items from the folder that meets the predefined condition, you need to sort the items in ascending order and set the IncludeRecurrences to true. You will not catch recurrent appointments if you don’t do this before using the Restrict method. Read more about that in the How To: Use Restrict method in Outlook to get calendar items article.
Also you may find the How To: Retrieve Outlook calendar items using Find and FindNext methods article helpful.
Dates and times are typically stored with a Date format, the Find and Restrict methods require that the date and time be converted to a string representation. To make sure that the date is formatted as Microsoft Outlook expects, use the Format function. The following example creates a filter to find all contacts that have been modified after January 15, 2022 at 3:30 P.M.
sFilter = "[LastModificationTime] > '" & Format("1/15/2022 3:30pm", "ddddd h:nn AMPM") & "'"
I have a problem, the macro below checks if there is a mail title from the sender. The problem is that in outlook, I have 2 accounts: IMAP and Exchange. Makro always chooses exchange. Where's the reason?
Sub srchmail()
Dim Itms As Items
Dim srchSender As String
Dim srchSubject As String
Dim strFilterBuild As String
Dim ItmsBuild As Items
Dim strFilter As String
Set olApp = Outlook.Application
Set objNS = olApp.GetNamespace("MAPI")
Set sub_olFolder = objNS.GetDefaultFolder(olFolderInbox)
Set sub_olFolder = sub_olFolder.Folders("SUBFOLDER")
Set Itms = sub_olFolder.Items
Dim i As Long
For i = 1 To Itms.Count
Next
srchSender = "SENDERMAIL"
srchSubject = "SUBJECTMAIL"
strFilter = "[SenderName] = '" & srchSender & "' And [Subject] = '" & srchSubject & "' And [SentOn] > '" & Format(Date, "dd-mm-yyyy") & "'"
Set Itms = Itms.Restrict(strFilter)
If Itms.Count = 0 Then
MsgBox "dont mail in " & sub_olFolder & " with sub:'" & srchSubject & "' on " & Format(Date, "dd-mm-yyyy")
Else
MsgBox "found mail in " & sub_olFolder & " with sub: '" & srchSubject & "' on " & Format(Date, "dd-mm-yyyy")
End If
ExitRoutine:
Set Itms = Nothing
End Sub
Your problem is:
Set sub_olFolder = objNS.GetDefaultFolder(olFolderInbox)
You can only have one default Inbox. Your default Inbox is clearly in the Exchange account. You will need to explicitly name the IMAP store to access its Inbox.
When you look at your folder pane, you will see something like this:
Xxxxxxxxxxx
Drafts
Deleted Items
Inbox
: :
Yyyyyyyyy
Drafts
Deleted Items
Inbox
SUBFOLDER
: :
Xxxxxxxxxxx and Yyyyyyyyy are the names of stores. Stores are the files in which Outlook saves all your emails, calendar items, tasks and so on. As I understand it, you MUST have one store per account. You can also have as many extra stores as you wish. I have stores named for my two accounts, “Archive”, “Test” and many more.
Try:
Set sub_olFolder = objNS.Folders("Yyyyyyyyy").Folders("Inbox")
Where “Yyyyyyyyy” is the name of the store (as it appears in the folder pane) containing the sub folder you wish to access.
I should perhaps add that I would have written:
Set Itms = Session.Folders("Yyyyyyyyy").Folders("Inbox").Folders("SUBFOLDER").Items
“Session” and “NameSpace” are supposed to be identical. I use Session because it avoids the need to create a namespace and because a long time ago I had a failure with NameSpace that I could not diagnose.
Unless you need to access the store or Inbox or SUBFOLDER in some other way, you do not need variables for them. Starting with the ultimate parent (Session) you can string the names of children, grandchildren and so on to any depth.
Be careful stringing properties together like this. (1) It can take a little time to get the string correct. (2) I have seen example of property strings where it is really difficult to determine what it being accessed. If you return to this macro in 12 months, will you remember what this string means? If someone else has to maintain your macro, will they understand what you have done? If in doubt, keep it simple.
Added because of error in original answer
It is not possible for me to test my answer. Try the following as a way for you to test my suggestion.
Replace "Yyyyyyyyy" in the following by the name of your IMAP store and then type it in your Immediate Window.
? Session.Folders("Yyyyyyyyy").Name
The interpreter should respond with the name of the IMAP store. If that works, try:
? Session.Folders("Yyyyyyyyy").Folders("Inbox").Name
The interpreter should respond Inbox. If that works, try:
? Session.Folders("Yyyyyyyyy").Folders("Inbox").Folders("SUBFOLDER").Name
The interpreter should respond SUBFOLDER. If that works, try:
? Session.Folders("Yyyyyyyyy").Folders("Inbox").Folders("SUBFOLDER").Items(1).ReceivedTime
The interpreter should respond with the date and time of the older email in the subfolder. If that works, my answer should work. If any of the above doesn't work, please respond with a detailed explanation of what failed
It looks like your Exchange account is set as default, so
when you get sub_olFolder, you are working with subfolder of Inbox of the message store that linked with Exchange account.
My VBA experience is incredibly limited.I have created basic macros for excel primarily by frankensteining multiple macros I find online together.
Here's what I am looking to do. Every morning I send out an email to a list of 200 customers, I open the new message from a list and the message auto populates (as it is a signature). Currently I then go through all these emails and add my subject and BCC. Could I possibly create a macro to open all of these emails, add my BCC, add my subject, and then send the emails.
Any and all help is much appreciated.
The following code defines an instance of Outlook.Application, and sets up a MailItem ready for sending. It uses a Dictionary object called EmailData to hold the various bits of info to populate To, BCC etc but those can be replaced with your own strings etc. I've pulled this from a function I wrote and made it a little more generic:
Public Function OL_SendMail()
Dim bOpenedOutlook, sComputer, iLoop, iAccount, sAttachArray, sAttachment
bOpenedOutlook = False
sComputer = "."
Dim oWMIService : Set oWMIService = GetObject("winmgmts:\\" & sComputer & "\root\cimv2")
Dim colItems : Set colItems = oWMIService.ExecQuery ("Select * from Win32_Process Where Name = 'outlook.exe'")
Dim oOutlook : Set oOutlook = CreateObject("Outlook.Application")
Dim oNamespace : Set oNamespace = oOutlook.GetNamespace("MAPI")
If colItems.Count = 0 Then
' Outlook isn't open, logging onto it...
oNamespace.Logon "Outlook",,False,True
bOpenedOutlook = True
End If
Dim oFolder : Set oFolder = oNamespace.GetDefaultFolder(olFolderInbox)
If EmailData("SendFrom") = "" Then
' default to first email account the user has access to
iAccount = 1
Else
' Checking to see if the account to send from is accessible by this user...
iAccount = 0
For iLoop = 1 To oOutlook.Session.Accounts.Count
If UCase(Trim(oOutlook.Session.Accounts.Item(iLoop))) = UCase(Trim(EmailData("SendFrom"))) Then
iAccount = iLoop
Exit For
End If
Next
If iAccount = 0 Then
sErrorMsg = "Cannot send email from specified account: " & EmailData("SendFrom") & " as this user doesn't appear to have access to it in Outlook!"
OL_SendMail = False
Exit Function
End If
End If
Dim oMailItem : Set oMailItem = oOutlook.CreateItem(olMailItem)
With oMailItem
Set .SendUsingAccount = oOutlook.Session.Accounts.Item(iAccount)
.To = EmailData("To")
.CC = EmailData("CC")
.BCC = EmailData("BCC")
.Subject = EmailData("Subject")
.Body = EmailData("Body")
sAttachArray = Split(EmailData("AttachmentPaths"), ";")
For Each sAttachment In sAttachArray
.Attachments.Add(sAttachment)
Next
.Recipients.ResolveAll
.Display ' debug mode - uncomment this to see email before it's sent out
End With
'Mail Item created and ready to send
'oMailItem.Send ' this is commented out so the mail doesn't auto send, allows checking of it!!
Set oMailItem = Nothing
Set oNamespace = Nothing
If bOpenedOutlook Then
'oOutlook.Quit
End If
Set oOutlook = Nothing
Set colItems = Nothing
Set oWMIService = Nothing
OL_SendMail = True
End Function
I have code in Access that gets all emails in the user's Inbox that are sent by an individual email address. This code (simplified, below) works fine:
Dim outItems as Outlook.Items
Dim strEMAddress as string
Dim outFolder as Outlook.MAPIFolder
Set outFolder = outNS.GetDefaultFolder(olFolderInbox)
Set outItems = outFolder.Items
str="my#email.com"
Set outItems = outItems.Restrict("[SenderEmailAddress] = " & "'" & strEMAddress & "'")
I am looking for something that will do likewise on the SentMails folder, restricting the items to those sent to a specific email address.
I know this is complicated by the fact that .Recipients is a collection (as items can/do have more than one recipient). I am hoping there is a way to return a list of items that contain the email address I am looking for in any of the sent fields (To/CC/bcc - but happy with just To if this is easier).
I have searched online and found .To is no good (is not the email address) and I can't get pseudo code such as this work:
Set outItems = outItems.Restrict("[Recipients] = " & "'" & strEMAddress & "'")
You can use the DASL query as the filter string in your items.restrict method.
For example to find all mails i sent to Ali Raza i use the following
str_fltr = "#SQL=""urn:schemas:httpmail:displayto"" ci_phrasematch '%Ali Raza%'"
The good thing about the above DASL query is that it returns matches with multiple recepients whether if you use the jet syntax for searching resultx will only contain items with one recipient. Jet syntax is the one that you are currently using. You should use the [To] property rather than [Recipients]
Here https://msdn.microsoft.com/en-us/library/cc513841%28v=office.12%29.aspx#SearchingOutlookData_Overview is good place where you can learn almost everything about searching in outlook.
Here http://www.msoffice.us/Outlook/PDF/%28Outlook%202010%29%20Common%20DASL%20Property%20Tags.pdf is a list of common DASL tags which will come in handy if you get a grip on DASL syntax.
For multiple [TO/CC/BCC] filter example would be...
Option Explicit
Public Sub Example()
Dim olNs As Outlook.NameSpace
Dim Folder As Outlook.MAPIFolder
Dim Items As Outlook.Items
Dim Filter As String
Dim Msg As String
Dim i As Long
Set olNs = Application.GetNamespace("MAPI")
Set Folder = olNs.GetDefaultFolder(olFolderSentMail)
Filter = "#SQL=" & "urn:schemas:httpmail:displayto" & _
" Like '%John Doe%' Or " & _
"urn:schemas:httpmail:displaycc" & _
" Like '%John Doe%' Or " & _
"urn:schemas:httpmail:displaybcc" & _
" Like '%John Doe%'"
Set Items = Folder.Items.Restrict(Filter)
Msg = Items.Count & " Items in " & Folder.Name & " Folder"
If MsgBox(Msg, vbYesNo) = vbYes Then
For i = Items.Count To 1 Step -1
Debug.Print Items(i) 'Immediate Window
Next
End If
End Sub
now remember if the display name is John.Doe#Email.com then filter should be %John.Doe#Email.com% else use %John Doe%
If using Redemption is an option (I am its author), you can use RDOFolder.Items.Restrict - unlike Outlook Object Model, it does expand To/CC/BCC queries into recipient sub restrictions on PR_DISPLAY_NAME and PR_EMAIL_ADDRESS properties on each recipient (RES_SUBRESTRICTION / PR_MESSAGE_RECIPIENTS / RES_OR / PR_DISPLAY_NAME | PR_EMAIL_ADDRESS).
set Session = CreateObject("Redemption.RDOSession")
Session.MAPIOBJECT = Application.Session.MAPIOBJECT
set Folder = Session.GetFolderFromID(Application.ActiveExplorer.CurrentFolder.EntryID)
set restrItems = Folder.Items.Restrict(" TO = 'user#domain.demo' ")
You can also specify Recipients property in the SQL query - it will be matched against recipients of all types (to/cc/bb):
set restrItems = Folder.Items.Restrict(" Recipients = 'user#domain.demo' ")
I have tried to make a script to pick up emails as they come in, reformat them and then forward on to the email in the body but I cannot work out how to read the email body. I currently have:
Sub Confirmation()
myMessage = "You recently made a request on the IT website, the details of your
request can be seen below:" & vbCr & vbCr & "Thank you, " & vbCr & "IT Support"
Dim itmOld As MailItem, itmNew As MailItem
Set itmOld = ActiveInspector.CurrentItem
Set itmNew = itmOld.Forward
itmNew.Body = myMessage & vbCr & vbCr & itmOld.Body
itmNew.Subject = "IT Web Request Confirmation"
itmNew.Display
Set itmOld = Nothing
Set itmNew = Nothing
End Sub
This opens the email adds some text to it and forwards it on.
I would like the script to open the email, read an email address from the body, use that as the to field and reformat the existing email to a nicer format.
This is the HTML from the email:
<html><body><br /><br /><table><tr><td><b>Fullname: </b></td><td>Alex Carter</td></tr><tr><td><b>OPS_Access: </b></td><td>Yes</td></tr><tr><td><b>Email_Account_Required: </b></td><td>Yes</td></tr><tr><td><b>Office_Email_Required: </b></td><td>Yes</td></tr><tr><td><b>Website_Access_Required: </b></td><td>Yes</td></tr><tr><td><b>Web_Access_Level: </b></td><td>Staff</td></tr><tr><td><b>Forum_Access_Required: </b></td><td>Yes</td></tr><tr><td><b>Date_Account_Required: </b></td><td>03/08/2013</td></tr><tr><td><b>Requested_By: </b></td><td>Alex Carter</td></tr><tr><td><b>Requestee_Email: </b></td><td>alex.carter#cars.co.uk</td></tr><tr><td><b>Office_Requesting: </b></td><td>Swindon</td></tr></table></body></html>
This shows that the email to go into the to field is in the 10th row of the table but I am not too sure how to go about selecting this from the body?
How would I go about reading the body, reformatting it and then selecting the requestee email and using it as the to field?
Thanks in advance!
This should help you get started (modifying your code), though you'll have to be more specific with regard to what formatting improvements you would like to see...:
Sub Confirmation()
myMessage = "You recently made a request on the IT website, the details of your request can be seen below:" & vbCr & vbCr & "Thank you, " & vbCr & "IT Support"
Dim sAddress As String ' Well need this to store the address
Dim itmOld As MailItem, itmNew As MailItem
Set itmOld = ActiveInspector.CurrentItem
Set itmNew = itmOld.Forward
sAddress = GetAddressFromMessage(itmOld) ' This is our new function
If Len(sAddress) > 0 Then
itmNew.To = sAddress ' If our new function found a value apply it to the To: field.
'!!! This should be checked as a valid address before continuing !!!
End If
itmNew.Body = myMessage & vbCr & vbCr & itmOld.Body
itmNew.Subject = "IT Web Request Confirmation"
itmNew.Display
Set itmOld = Nothing
Set itmNew = Nothing
End Sub
Private Function GetAddressFromMessage(msg As MailItem) As String
' Grabs the email from the standard HTML form described in the SO question.
Dim lStart As Long
Dim lStop As Long
Dim sItemBody As String
Const sSearchStart As String = "Requestee_Email: </b></td><td>" ' We will look for these tags to determine where the address can be found.
Const sSearchStop As String = "</td>"
sItemBody = msg.HTMLBody ' Read the body of the message as HTML to retain TAG info.
lStart = InStr(sItemBody, sSearchStart) + Len(sSearchStart)
If lStart > 0 Then ' Make sure we found the first TAG.
lStop = InStr(lStart, sItemBody, sSearchStop)
End If
GetAddressFromMessage = vbNullString
If lStop > 0 And lStart > 0 Then ' Make sure we really did find a valid field.
GetAddressFromMessage = Mid(sItemBody, lStart, lStop - lStart)
End If
End Function