Add attachment to all selected items in Outlook 2016 with VBA - vba

I aim to add an attachment to every item that is currently selected in Outlook 2016. My idea is to call Attachments.Add in a loop on each item in the current selection.
In my Drafts folder, I have three drafts with the subjects:
Draft Test 3
Draft Test 2
Draft Test 1
Because of the environment that I am in, I cannot use C#. I am using VBA instead. I ran all the test code by clicking Developer > Macros > [sub name] in the Outlook 2016 ribbon.
I started with this:
Sub AddTestTxtToSelection1()
Dim i As Long
With Application.ActiveExplorer.Selection
For i = .Count To 1 Step -1
.Item(i).Attachments.Add "C:\Full\Path\To\Test.txt", olByValue, 1
Next
End With
End Sub
Unfortunately, Test.txt was only attached to Draft Test 3 although all three drafts were selected. I thought that I might be iterating through the selection incorrectly, so I tried this:
Sub AddTestTxtToSelection2()
For Each objMessage In Application.ActiveExplorer.Selection
objMessage.Attachments.Add "C:\Full\Path\To\Test.txt", olByValue, 1
Next
End Sub
Again, although all three drafts were selected, Test.txt was only attached to Draft Test 3. In the example code in this article, Application.ActiveExplorer and its Selection property are stored in separate variables. I thought that that might have been what was missing, so I wrote this:
Sub AddTestTxtToSelection3()
Dim myOlExp As Explorer
Dim myOlSel As Selection
Set myOlExp = Application.ActiveExplorer
Set myOlSel = myOlExp.Selection
Dim i As Long
For i = 1 To myOlSel.Count
myOlSel.Item(i).Attachments.Add "C:\Full\Path\To\Test.txt", olByValue, 1
Next
End Sub
The behavior was identical to that of the first two tests. Finally, it occurred to me that the problem might be with modifying the drafts as I was looping over them. I then wrote this code, which stores the EntryID properties of the selected items in a separate string array before looping over them:
Sub AddTestTxtToSelection4()
Dim i As Long
Dim strEntryID As Variant
Dim namespaceMAPI As NameSpace
Dim objMessage As Object
Dim selected() As String
' Copy the current selection into an array of EntryID strings.
ReDim selected(1 To Application.ActiveExplorer.Selection.Count) As String
For i = 1 To Application.ActiveExplorer.Selection.Count
selected(i) = Application.ActiveExplorer.Selection.Item(i).EntryID
Next
' Retrieve each item from its EntryID string.
Set namespaceMAPI = Application.GetNamespace("MAPI")
namespaceMAPI.Logon
For Each strEntryID In selected
Set objMessage = namespaceMAPI.GetItemFromID(strEntryID)
objMessage.Attachments.Add "C:\Full\Path\To\Test.txt", olByValue, 1
Next
End Sub
Again, only Draft Test 3 had Test.txt attached after running this code. I thought that Outlook might be having trouble attaching the same file to multiple drafts, so I modified the last test to attach a different file to each draft. Only Draft Test 3 had an attachment after it was executed. Even if I swap out Application.ActiveExplorer.Selection for Application.ActiveExplorer.CurrentFolder.Items, still only the first draft gets an attachment.
Why can't Outlook attach a file to more than one mail item at a time? Is there a workaround?

Certain actions require a .Save.
There is likely a correlation with actions that require a save when done manually. In this case if you were to manually attach a file then close the draft you would be asked if the draft should be saved.

I have accepted #niton's answer, but here is my code after adding .Save:
' Based on AddTestTxtToSelection2
Sub AddTestTxtToSelection5()
For Each objMessage In Application.ActiveExplorer.Selection
objMessage.Attachments.Add "C:\Full\Path\To\Test.txt", olByValue, 1
objMessage.Save ' This line was added.
Next
End Sub
The attachments get added to every selected message now.

Related

Filter Outlook 2010 tasks using VBA

I need to create custom filters in Outlook to save me from having to manually adjust the filter setting each time, preferably with VBA.
Below is my attempt. I inserted the message box line to check the correct items are being restricted. On running the macro I get a number of message boxes displayed with "1" indicating to me that it is working as expected (message box appears for each 'In Progress' item).
For Each Task_List In CreateObject("Outlook.Application").GetNamespace("MAPI").GetDefaultFolder(13).Items.Restrict("[Status]='In Progress'")
MsgBox Task_List.Status
sFilter = "[Status]=Task_List.Status"
Next
However, the tasks in the task folder are not filtered, all the tasks are displayed regardless of criteria.
What am I missing from my code? Or am I completely barking up the wrong tree.
Thanks, and apologies in advance for the simplistic question.
Once you manually set up different views you can get to them this way.
Where the view is named for instance "In Progress"
Sub TaskView_InProgress()
' No error if the view does not exist
' No error if not currently in Tasks folder
ActiveExplorer.CurrentView = "In Progress"
End Sub
This demonstrates how to access the In Progress tasks. Albeit much less helpful than a view if you have many tasks.
Private Sub task_Filter()
' Folders may contain any type of item
Dim myItem As Object
Dim myItems As items
Dim resItems As items
Dim myTaskFolder As Folder
Dim sFilter As String
Dim msgPrompt As String
Set myTaskFolder = Session.GetDefaultFolder(olFolderTasks)
Set myItems = myTaskFolder.items
sFilter = "[Status]='In Progress'"
Set resItems = myItems.Restrict(sFilter)
For Each myItem In resItems
If myItem.Class = OlTask Then
myItem.Display
End If
Next
End Sub
This sub worked great for my purpose. I wanted to also input a string in the search field of the task window from excel. So I loaded the string to the clipboard and used send keys to "Ctrl E" (enter search field) then "Ctrl V" paste. This routine turns num lock off. So I added a toggle for that.
Sub btn_GotoTask()
Set cl = New clsClient
' Folders may contain any type of item
Dim myItem As Object
Dim myItems As items
Dim resItems As items
Dim myTaskFolder As Folder
Dim sFilter As String
Dim msgPrompt As String
On Error GoTo outlookError
Set myTaskFolder = Session.GetDefaultFolder(olFolderTasks)
myTaskFolder.Display
SetClipboard cl.Pol
'Activate task window
myTaskFolder.Application.ActiveWindow
SendKeys "^{e}"
SendKeys "^{v}"
SendKeys "{NUMLOCK}"
Exit Sub
outlookError:
MsgBox "Outlook may not be open"
End Sub

For Each loop: Some items get skipped when looping through Outlook mailbox to delete items

I wanted to develop VBA code that:
Loops through all email items in mailbox
If there are any type of other items say "Calendar Invitation" skips that item.
Finds out the emails with attachments
If attached file has ".xml" extension and a specific title in it, saves it to a directory, if not it keeps searching
Puts all email includes .xml attachments to "Deleted Items" folder after doing step 4 and deletes all emails in that folder by looping.
Code works perfect EXCEPT;
For example
There are 8 email received with ".xml" file attached to each one of them in your mailbox.
run the code
you will see only 4 of the 8 items are processed successfully, other 4 remain in their positions.
If you run the code again, now there would be 2 items processed successfully and other 2 remain in your mailbox.
Problem: After running the code, it is supposed to process all files and deletes them all not the half of them in each run. I want it to process all items at a single run.
BTW, this code runs every time I open the Outlook.
Private Sub Application_Startup()
'Initializing Application_Startup forces the macros to be accessible from other offic apps
'Process XML emails
Dim InboxMsg As Object
Dim DeletedItems As Outlook.Folder
Dim MsgAttachment As Outlook.Attachment
Dim ns As Outlook.NameSpace
Dim Inbox As Outlook.Folder
Dim fPathTemp As String
Dim fPathXML_SEM As String
Dim fPathEmail_SEM As String
Dim i As Long
Dim xmlDoc As New MSXML2.DOMDocument60
Dim xmlTitle As MSXML2.IXMLDOMNode
Dim xmlSupNum As MSXML2.IXMLDOMNode
'Specify the folder where the attachments will be saved
fPathTemp = "some directory, doesn't matter"
fPathXML_SEM = "some directory, doesn't matter"
fPathEmail_SEM = "some directory, doesn't matter"
'Setup Outlook
Set ns = GetNamespace("MAPI")
Set Inbox = ns.Folders.Item("mailbox-name").Folders("Inbox")
Set DeletedItems = ns.Folders.Item("mailbox-name").Folders("Deleted Items")
'Loop through all Items in Inbox, find the xml attachements and process if they are the matching reponses
'On Error Resume Next
For Each InboxMsg In Inbox.Items
If InboxMsg.Class = olMail Then 'if it is a mail item
'Check for xml attachement
For Each MsgAttachment In InboxMsg.Attachments
If Right(MsgAttachment.DisplayName, 3) = "xml" Then
'Load XML and test for the title of the file
MsgAttachment.SaveAsFile fPathTemp & MsgAttachment.FileName
xmlDoc.Load fPathTemp & MsgAttachment.FileName
Set xmlTitle = xmlDoc.SelectSingleNode("//title")
Select Case xmlTitle.Text
Case "specific title"
'Get supplier number
Set xmlSupNum = xmlDoc.SelectSingleNode("//supplierNum")
'Save the XML to the correct folder
MsgAttachment.SaveAsFile fPathXML_SEM & xmlSupNum.Text & "_" & Format(Date, "yyyy-mm-dd") & ".xml"
'Save the email to the correct folder
InboxMsg.SaveAs fPathEmail_SEM & xmlSupNum.Text & "_" & Format(Date, "yyyy-mm-dd") & ".msg"
'Delete the message
InboxMsg.Move DeletedItems
Case Else
End Select
'Delete the temp file
On Error Resume Next
Kill fPathTemp & MsgAttachment.FileName
On Error GoTo 0
'Unload xmldoc
Set xmlDoc = Nothing
Set xmlTitle = Nothing
Set xmlSupNum = Nothing
End If
Next
End If
Next
'Loop through deleted items and delete
For Each InboxMsg In DeletedItems.Items
InboxMsg.Delete
Next
'Clean-up
Set InboxMsg = Nothing
Set DeletedItems = Nothing
Set MsgAttachment = Nothing
Set ns = Nothing
Set Inbox = Nothing
i = 0
End Sub
Likely cause: When you do this InboxMsg.Move, all of the messages in your inbox after the one that was moved are bumped up by one position in the list. So you end up skipping some of them. This is a major annoyance with VBA's For Each construct (and it doesn't seem to be consistent either).
Likely solution: Replace
For Each InboxMsg In Inbox.Items
with
For i = Inbox.Items.Count To 1 Step -1 'Iterates from the end backwards
Set InboxMsg = Inbox.Items(i)
This way you iterate backward from the end of the list. When you move a message to deleted items, then it doesn't matter when the following items in the list are bumped up by one, because you've already processed them anyway.
It's often not a good idea to modify the contents of a (sub)set of items while looping over them. You could modify your code so that it first identifies all of the items that need to be processed, and adds them to a Collection. Then process all the items in that collection.
Basically you shouldn't be removing items from the Inbox while you're looping through its contents. First collect all the items you want to process (in your Inbox loop), then when you're done looping, process that collection of items.
Here's some pseudo-code which demonstrates this:
Private Sub Application_Startup()
Dim collItems As New Collection
'Start by identifying messages of interest and add them to a collection
For Each InboxMsg In Inbox.Items
If InboxMsg.Class = olMail Then 'if it is a mail item
For Each MsgAttachment In InboxMsg.Attachments
If Right(MsgAttachment.DisplayName, 3) = "xml" Then
collItems.Add InboxMsg
Exit For
End If
Next
End If
Next
'now deal with the identified messages
For Each InboxMsg In collItems
ProcessMessage InboxMsg
Next InboxMsg
'Loop through deleted items and delete
For Each InboxMsg In DeletedItems.Items
InboxMsg.Delete
Next
End Sub
Sub ProcessMessage(InboxMsg As Object)
'deal with attachment(s) and delete message
End Sub

Find and Select an Outlook Email from MS Access

I need to build a tool that will allow the user to select an email from his Outlook so I can then save that email as a .msg file or alternately save just the attachment as a file.
I'm stumbling a little bit over what might be the easiest and the best way to allow searching/filtering of emails. I need to give the user a view that is at least slightly similar to Outlook (for example, folders should be the same order/hierarchy.
Does the Outlook Object Model have some kind of Explorer/Picker/Selection dialog I can call that will return a storeid and an entryid after the user selects an email? Or do I need to roll my own?
I should mention that I already know how to save the email or attachment so my question is only about handling selection and filtering of emails.
FYI, I'm programming this in MS Access 2007 with Outlook 2007. The target machines have either 2007 or 2010 versions of Access and Outlook.
Linking to the Outlook table is fine. The problem is that Outlook doesn't provide a unique ID to each message and if the message is moved from one folder to another, its ID changes. Clearly not designed by someone who understands databases.
A better approach may be to create an Outlook add-in that runs within Outlook, then performs the tasks you need to send the info to Access.
I rarely program with Access but I moved some code across from Outlook, hacked it around a bit and it seems to work. This is not a solution but it should show you how to access all the information you need.
I had one problem. Neither Set OutApp = CreateObject("Outlook.Application") nor Set OutApp = New Outlook.Application create a new instance of Outlook if one is already open. So Quit closes Outlook whether or not it was open before the macro started. I suggest you post a new question on this issue; I am sure someone knows how to tell if Outlook is already open and therefore not to quit it.
The folder structure in Outlook is slightly awkward because the top level folders are of type Folders while all sub-folders are of type MAPIFolder. Once you have got past that it is fairly straightforward.
The code below includes function GetListSortedChildren(ByRef Parent As MAPIFolder) As String. This function finds all the children of Parent and returns a string such as "5,2,7,1,3,6,4" which lists the indices for the children in ascending sequence by name. I would use something like this to populates a ListView by expanding nodes as the user required.
I have provided a subroutine CtrlDsplChld() which controls the output to the immediate windows of all the folders in sequence. I believe that should give you enough guidance to get started on accessing the folder hierarchy.
Subroutine DsplChld(ByRef Parent As MAPIFolder, ByVal Level As Long) includes code to find the first message with attachments. This will you tell you how to look through a folder for a particular message.
Finally, CtrlDsplChld() displayes selected properties of the message: Subject, To, HTMLBody and the display names of the attachments.
Hope this helps.
Option Compare Database
Option Explicit
Dim ItemWithMultipleAttachments As Outlook.MailItem
Sub CtrlDsplChld()
Dim ArrChld() As String
Dim ListChld As String
Dim InxAttach As Long
Dim InxChld As Long
Dim InxTopLLCrnt As Long
Dim OutApp As Outlook.Application
Dim TopLvlList As Folders
Set ItemWithMultipleAttachments = Nothing
Set OutApp = CreateObject("Outlook.Application")
'Set OutApp = New Outlook.Application
With OutApp
Set TopLvlList = .GetNamespace("MAPI").Folders
For InxTopLLCrnt = 1 To TopLvlList.Count
' Display top level children and their children
Call DsplChld(TopLvlList.Item(InxTopLLCrnt), 0)
Next
If Not ItemWithMultipleAttachments Is Nothing Then
With ItemWithMultipleAttachments
Debug.Print .Subject
Debug.Print .HTMLBody
Debug.Print .To
For InxAttach = 1 To .Attachments.Count
Debug.Print .Attachments(InxAttach).DisplayName
Next
End With
End If
.Quit
End With
Set OutApp = Nothing
End Sub
Sub DsplChld(ByRef Parent As MAPIFolder, ByVal Level As Long)
Dim ArrChld() As String
Dim InxChld As Long
Dim InxItemCrnt As Long
Dim ListChld As String
Debug.Print Space(Level * 2) & Parent.Name
If ItemWithMultipleAttachments Is Nothing Then
' Look down this folder for a mail item with an attachment
For InxItemCrnt = 1 To Parent.Items.Count
With Parent.Items(InxItemCrnt)
If .Class = olMail Then
If .Attachments.Count > 1 Then
Set ItemWithMultipleAttachments = Parent.Items(InxItemCrnt)
Exit For
End If
End If
End With
Next
End If
ListChld = GetListSortedChildren(Parent)
If ListChld <> "" Then
' Parent has children
ArrChld = Split(ListChld, ",")
For InxChld = LBound(ArrChld) To UBound(ArrChld)
Call DsplChld(Parent.Folders(ArrChld(InxChld)), Level + 1)
Next
End If
End Sub
Function GetListSortedChildren(ByRef Parent As MAPIFolder) As String
' The function returns "" if Parent has no children.
' If the folder has children, the functions returns "P,Q,R, ..." where
' P, Q, R and so on indices of the children of Parent in ascending
' order by name.
Dim ArrInxFolder() As Long
'Dim ArrFolder() As MAPIFolder
Dim InxChldCrnt As Long
Dim InxName As Long
Dim ListChld As String
If Parent.Folders.Count = 0 Then
' No children
GetListSortedChildren = ""
Else
'ReDim ArrName(1 To Parent.Folders.Count)
'For InxChldCrnt = 1 To Parent.Folders.Count
' ArrFolder(InxChldCrnt) = Parent.Folders(InxChldCrnt)
'Next
Call SimpleSortMAPIFolders(Parent, ArrInxFolder)
ListChld = CStr(ArrInxFolder(1))
For InxChldCrnt = 2 To Parent.Folders.Count
ListChld = ListChld & "," & CStr(ArrInxFolder(InxChldCrnt))
Next
GetListSortedChildren = ListChld
End If
End Function
Sub SimpleSortMAPIFolders(ArrFolder As MAPIFolder, _
ByRef InxArray() As Long)
' On exit InxArray contains the indices into ArrFolder sequenced by
' ascending name. The sort is performed by repeated passes of the list
' of indices that swap adjacent entries if the higher come first.
' Not an efficient sort but adequate for short lists.
Dim InxIACrnt As Long
Dim InxIALast As Long
Dim NoSwap As Boolean
Dim TempInt As Long
ReDim InxArray(1 To ArrFolder.Folders.Count) ' One entry per sub folder
' Fill array with indices
For InxIACrnt = 1 To UBound(InxArray)
InxArray(InxIACrnt) = InxIACrnt
Next
If ArrFolder.Folders.Count = 1 Then
' One entry list already sorted
Exit Sub
End If
' Each repeat of the loop moves the folder with the highest name
' to the end of the list. Each repeat checks one less entry.
' Each repeats partially sorts the leading entries and may result
' in the list being sorted before all loops have been performed.
For InxIALast = UBound(InxArray) To 1 Step -1
NoSwap = True
For InxIACrnt = 1 To InxIALast - 1
If ArrFolder.Folders(InxArray(InxIACrnt)).Name > _
ArrFolder.Folders(InxArray(InxIACrnt + 1)).Name Then
NoSwap = False
' Move higher entry one slot towards the end
TempInt = InxArray(InxIACrnt)
InxArray(InxIACrnt) = InxArray(InxIACrnt + 1)
InxArray(InxIACrnt + 1) = TempInt
End If
Next
If NoSwap Then
Exit For
End If
Next
End Sub

Force the `Recipients` object to update when the To: text box has been edited

I sort recipients when composing an email.
If I have 3 recipients (for example), run the sort macro, and then remove a recipient from the To: text box, running the macro a second time causes the removed recipient to re-appear. When I step through the macro on the second run, I can see that both .CurrentItem.To and the Recipients object still have all 3 recipients.
It is intermittent. Is there any way to force the Recipients object to update when the To: text box has been edited?
I can't find anything in the Outlook VBA documentation and trial and error has proved fruitless.
Code excerpt:
Public Sub SortRecipients()
With Application.ActiveInspector
If TypeOf .CurrentItem Is Outlook.MailItem Then
Debug.Print "Before: "
Debug.Print "To: " & .CurrentItem.To
Debug.Print "# of recipients: " & .CurrentItem.Recipients.Count
' Force an update if recipients have changed (DOESN'T HELP)
.CurrentItem.Recipients.ResolveAll
Set myRecipients = .CurrentItem.Recipients
' Create objects for To list
Dim myRecipient As recipient
Dim recipientToList As Object
Set recipientToList = CreateObject("System.Collections.ArrayList")
' Create new lists from To line
For Each myRecipient In myRecipients
recipientToList.Add myRecipient.Name
Next
' Sort the recipient lists
recipientToList.Sort
' Remove all recipients so we can re-add in the correct order
While myRecipients.Count > 0
myRecipients.Remove 1
Wend
' Create new To line
Dim recipientName As Variant
For Each recipientName In recipientToList
myRecipients.Add (recipientName)
Next recipientName
.CurrentItem.Recipients.ResolveAll
End If
End With
End Sub
Steps to reproduce:
Add 4 recipients to the "To" line of a new email in Outlook 2007 (click "Check Names" to resolve the addresses.)
Run the SortRecipients macro. (Recipients are now sorted)
Delete one recipient, re-run the SortRecipients macro.
After doing this, I still have 4 recipients (the deleted one returns).
You can (and should) add an Option Explicit, Outlook would have told you that myRecipients was not declared at the begining of your code.
I added:
Dim myRecipients As Recipients
[EDIT] That wasn't enough to get the To field refreshed. I tried several things but eventually, i added a .CurrentItem.Save instead of your try of .CurrentItem.Recipients.ResolveAll
I think i made it work this way on my Outlook 2007.

How to remove a specific category on a selected mail in Outlook 2003 with Macro?

I am trying to transform my Outlook2003 into the closest thing to gmail.
I started to use categories, which are pretty similar to labels in gmail. I can assign categories automatically with rules, and I can add categories manually.
I have also created "search folders", that show all mails with a given category, if they are not in the Deleted Items or Sent Items folders. This part is almost like the Label views in gmail.
Two things are missing basically, which should be done with macros (VBA to be precise) which I'm totally inexperienced with. So hence my questions:
-Can someone show me a macro to remove the category "Inbox"?
That would act exactly like the Archive button in gmail. In fact I want to assign this macro to a toolbar button and call it Archive.
I have a rule that adds the Inbox category to all incoming mail. As I said, I have a search folder displaying all mails categorized as Inbox, and I also have an All Mail search folder, that displays all messages regardless whether they have the Inbox category. Exactly like gmail, just the easy archiving is missing.
-Can someone show me a macro that would delete the selected mail/mails and also would remove the Inbox category before deletion? I would replace the default delete button with this macro. (Somewhat less important, as in my search folders I can filter messages that are physically placed in the Deleted Items folder, but it would be more elegant not to have mails categorized as Inbox in the trash.
Many thanks in advance,
szekelya
I have used Sue's remove code from http://www.outlookcode.com/codedetail.aspx?id=1211
But this should work you
Added save ! Doh!
Sub UnAssignInboxCat()
Dim SelectedItems As Selection
Dim Item As MailItem
Set SelectedItems = Application.ActiveExplorer.Selection
For Each Item In SelectedItems
RemoveCat Item, "Inbox"
Item.Save
Next
Set SelectedItems = Nothing
Set Item = Nothing
End Sub
Sub DeleteAndUnAssignInboxCat()
Dim SelectedItems As Selection
Dim Item As MailItem
Set SelectedItems = Application.ActiveExplorer.Selection
For i = SelectedItems.Count To 1 Step -1
Set Item = SelectedItems.Item(i)
RemoveCat Item, "Inbox"
Item.Save
Item.Delete
Next
Set SelectedItems = Nothing
Set Item = Nothing
End Sub
Sub RemoveCat(itm, catName)
arr = Split(itm.Categories, ",")
If UBound(arr) >= 0 Then
' item has categories
For i = 0 To UBound(arr)
If Trim(arr(i)) = catName Then
' category already exists on item
' remove it
arr(i) = ""
'rebuild category list from array
itm.Categories = Join(arr, ",")
Exit Sub
End If
Next
End If
End Sub