Saving attachments results in memory errors - vba

I need to search through 9,000 emails and save the attachments with a certain timestamp (these are security camera feeds).
The code works on a small number of emails, but after about 20 the processing in Outlook appears to speed up significantly (attachments stop saving) and then Outlook hangs with a memory error.
My guess is the save step is not completed before the script moves to the next email in the target folder and therefore the backlog of saves becomes too large for Outlook.
' this function grabs the timestamp from the email body
' to use as the file rename on save in the following public sub
Private Function GetName(olItem As MailItem) As String
Const strFind As String = "Exact Submission Timestamp: "
Dim olInsp As Inspector
Dim wdDoc As Object
Dim oRng As Object
Dim strDate As String
With olItem
Set olInsp = .GetInspector
Set wdDoc = olInsp.WordEditor
Set oRng = wdDoc.Range
With oRng.Find
Do While .Execute(strFind)
oRng.Collapse 0
oRng.End = oRng.End + 23
strDate = oRng.Text
strDate = Replace(strDate, Chr(58), Chr(95))
GetName = strDate & ".jpg"
Exit Do
Loop
End With
End With
lbl_Exit:
Set olInsp = Nothing
Set wdDoc = Nothing
Set oRng = Nothing
Exit Function
End Function
Public Sub SaveAttachmentsToDisk24(MItem As outlook.MailItem)
Dim oAttachment As outlook.Attachment
Dim sSaveFolder As String
Dim strFname As String
sSaveFolder = "C:\Users\xxxxx\"
For Each oAttachment In MItem.Attachments
If oAttachment.FileName Like "*.jpg" Then
strFname = GetName(MItem)
oAttachment.SaveAsFile sSaveFolder & strFname
Set oAttachment = Nothing
Set MItem = Nothing
End If
Next oAttachment

There are other possibilities but my belief is that the memory errors are the result of creating Word objects and then not closing them. Om3r asked for more information but you ignored his requests making it impossible to provide a definitive answer. However, I wanted to prove it was possible to extract attachments from a large number of emails without problems so I have made some guesses.
I understand why you need a routine that will scan your Inbox for the backlog of 8,000 camera feed emails. I do not understand why you want to use an event to monitor your Inbox as well. I cannot believe this is a time critical task. Why not just run the scan once or twice a day? However, the routine I have coded could be adapted to create a macro to be called by an event routine. My current code relies of global variables which you will have to change to local variables. I am not a fan of global variables but I did not want to create a folder reference for every call of the inner routine and the parameter list for a macro that might be called by an event routine is fixed.
To test the code I planned to create, I first generated 790 emails to myself that matched (I hope) your camera feed emails. I had planned to create more but I think my ISP has classified me as a spammer, or perhaps a flamer, and it would not let me send any more. The body of these emails looked like:
xxx Preamble xxx ‹cr›‹lf›|
Exact Submission Timestamp: 2019-02-22 15:00:00 ‹cr›‹lf›|
xxx Postamble xxx ‹cr›‹lf›|
Your code requires the string “Exact Submission Timestamp:” followed by a date which you use as a file name. I have assumed that date in in a format that VBA can recognise as a date and I have assumed the date is ended by a standard Windows newline (carriage return, line feed). The second assumption would be easy to change. I have a routine that will accept many more date formats than VBA’s CDate which I can provide if necessary.
Each email has a different date and time between November, 2018 and February, 2019.
I would never save 8,000 files in a single disc folder. Even with a few hundred files in a folder, it becomes difficult to find the one you want. My root folder is “C:\DataArea\Test” but you can easily change that. Given the timestamp in my example email, my routine would check for folder “C:\DataArea\Test\2019” then “C:\DataArea\Test\2019\02” and finally “C:\DataArea\Test\2019\02\22”. If a folder did not exist, it would be created. The attachment is then saved in the inner folder. My code could easily be adapted to save files at the month level or the hour level depending on how many of these files you get per month, day or hour.
My routine checks every email in Inbox for the string “Exact Submission Timestamp:” followed by a date. If it finds those, it checks for an attachment with an extension of JPG. If the email passes all these tests, the attachment is saved in the appropriate disc folder and the email is moved from Outlook folder “Inbox” to “CameraFeeds1”. The reasons for moving the email are: (1) it clears the Inbox and (2) you can rerun the routine as often as you wish without finding an already processed email. I named the destination folder “CameraFeeds1” because you wrote that you wanted to do some more work on these emails. I thought you could move the emails to folder “CameraFeeds2” once you had completed this further work.
I assumed processing 790 or 8,000 emails would take a long time. In my test, the duration was not as bad as I expected; 790 emails took about one and a half minutes. However, I created a user form to show progress. I cannot include the form in my answer so you will have to create your own. Mine looks like:
The appearance is not important. What is important is the name of the form and the four controls on the form:
Form name: frmSaveCameraFeeds
TextBox name: txtCountCrnt
TextBox name: txtCountMax
CommandButton name: cmdStart
CommandButton name: cmdStop
If you run the macro StartSaveCameraFeeds it will load this form. Click [Start] to start the save process. You can let the macro run until it has checked every email in the Inbox or you can click [Stop] at any time. The stop button is not as important as I feared. I thought the routine might take hours but that was not the case.
You don’t report where your 8,000 emails are. I have an Inbox per account plus the default Inbox which I only use for testing. I moved the 790 test emails to the default Inbox and used GetDefaultFolder to reference it. I assume you know how to reference another folder if necessary. Note I use Session instead of a name space. These two methods are supposed to be equivalent but I always use Session because it is simpler and because I once had a failure with a name space that I could not diagnose. I reference folder “CameraFeeds1” relative to the Inbox.
You will have to adjust my code at least partially. For the minimum changes, do the following:
Create a new module and copy this code into it:
Option Explicit
Public Const Marker As String = "Exact Submission Timestamp: "
Public Const RootSave As String = "C:\DataArea\Test"
Public FldrIn As Outlook.Folder
Public FldrOut As Outlook.Folder
Sub StartSaveCameraFeeds()
' Reference outlook folders then pass control to frmSaveCameraFeeds
Set FldrIn = Session.GetDefaultFolder(olFolderInbox)
Set FldrOut = FldrIn.Parent.Folders("CameraFeeds1")
Load frmSaveCameraFeeds
With frmSaveCameraFeeds
.Caption = "Saving jpg files from Camera feed emails"
.txtCountCrnt = 0
.txtCountMax = FldrIn.Items.Count
.Show vbModal
End With
' Form unloaded by cmdStop within form
Set FldrIn = Nothing
Set FldrOut = Nothing
End Sub
Public Sub SaveCameraFeed(ByRef ItemCrnt As MailItem)
' Checks a single mail item to be a "camera feed" email. If the mail item is
' a "camera feed" email, it saves the JPG file using the date within the
' email body as the file name. If the mail item is not a "camera feed"
' email, it does nothing.
' To be a camera feed mail item:
' * The text body must contain a string of the form: "xxxyyyy" & vbCr & vbLf
' where "xxx" matches the public constant Marker and "yyy" is recognised
' by VBA as a date
' * It must have an attachment with an extension of "JPG" or "jpg".
' If the mail item is a camera feed email:
' * In "yyy" any colons are replaced by understores.
' * The JPG attachment is saved with the name yyy & ".jpg"
Dim DateCrnt As Date
Dim DateStr As String
Dim DayCrnt As String
Dim InxA As Long
Dim MonthCrnt As String
Dim PathFileName As String
Dim PosEnd As Long
Dim PosStart As Long
Dim SomethingToSave As Boolean
Dim YearCrnt As String
SomethingToSave = False ' Assume no JPG to save until find otherwise
With ItemCrnt
PosStart = InStr(1, .Body, Marker)
If PosStart > 0 Then
PosStart = PosStart + Len(Marker)
PosEnd = InStr(PosStart, .Body, vbCr & vbLf)
DateStr = Mid$(.Body, PosStart, PosEnd - PosStart)
If IsDate(DateStr) Then
DateCrnt = DateStr
For InxA = 1 To .Attachments.Count
If LCase(Right$(.Attachments(InxA).Filename, 4)) = ".jpg" Then
SomethingToSave = True
Exit For
End If
Next
End If
End If
If SomethingToSave Then
DateStr = Replace(DateStr, ":", "_")
YearCrnt = Year(DateCrnt)
MonthCrnt = Month(DateCrnt)
DayCrnt = Day(DateCrnt)
Call CreateDiscFldrIfItDoesntExist(RootSave, YearCrnt, MonthCrnt, DayCrnt)
PathFileName = RootSave & "\" & YearCrnt & "\" & MonthCrnt & "\" & DayCrnt & _
"\" & Trim(DateStr) & ".jpg"
.Attachments(InxA).SaveAsFile PathFileName
.Move FldrOut
End If
End With
End Sub
Public Sub CreateDiscFldrIfItDoesntExist(ByVal Root As String, _
ParamArray SubFldrs() As Variant)
' If a specified disk folder (not an Outlook folder) does not exist, create it.
' Root A disk folder which must exist and for which the user
' must have write permission.
' SubFldrs A list of sub-folders required within folder Root.
' Example call: CreateDiscFldrsIfNecessary("C:\DataArea", "Aaa", "Bbb", "Ccc")
' Result: Folder "C:\DataArea\Aaa\Bbb\Ccc" will be created if it does not already exist.
' Note: MkDir("C:\DataArea\Aaa\Bbb\Ccc") fails unless folder "C:\DataArea\Aaa\Bbb" exists.
Dim Filename As String
Dim Fldrname As String
Dim InxSF As Long
Fldrname = Root
For InxSF = LBound(SubFldrs) To UBound(SubFldrs)
Fldrname = Fldrname & "\" & SubFldrs(InxSF)
If Not PathExists(Fldrname) Then
Call MkDir(Fldrname)
End If
Next
End Sub
Public Function PathExists(ByVal Pathname As String) As Boolean
' Returns True if path exists
' Coded by Tony Dallimore
' Based on code written by iDevlop: http://stackoverflow.com/a/28237845/973283
On Error Resume Next
PathExists = ((GetAttr(Pathname) And vbDirectory) = vbDirectory)
On Error GoTo 0
End Function
I must warn you that I have modules full of standard routines that I use all the time. I believe I have included all the standard routines used by the code I have written for you. If the code fails because a sub or function is missing, post a comment and I will apologise and add the missing macro to my code.
Near the top of the above code is Public Const RootSave As String = "C:\DataArea\Test". You will have to change this to reference your root folder.
The first statement of Sub StartSaveCameraFeeds() is Set FldrIn = Session.GetDefaultFolder(olFolderInbox). Amend this as necessary if the emails are not in the default Inbox.
In the body of Sub StartSaveCameraFeeds() you will find PosEnd = InStr(PosStart, .Body, vbCr & vbLf). If the date string is not ended by a standard Windows’ newline, amend this statement as necessary.
Create a user form. Add two TextBoxes and two CommandButtons. Name them as defined above. Copy the code below to the code area of the form:
Option Explicit
Private Sub cmdStart_Click()
' Call SaveCameraFeed for every MailItem in FldrIn
Dim CountMax As Long
Dim InxI As Long
Dim MailItemCrnt As MailItem
With FldrIn
CountMax = FldrIn.Items.Count
For InxI = CountMax To 1 Step -1
If .Items(InxI).Class = olMail Then
Set MailItemCrnt = .Items(InxI)
Call SaveCameraFeed(MailItemCrnt)
Set MailItemCrnt = Nothing
End If
txtCountCrnt = CountMax - InxI + 1
DoEvents
Next
End With
Unload Me
End Sub
Private Sub cmdStop_Click()
Unload Me
End Sub
The form code should not need amendment.
As I have already written, this code processed 790 camera feed emails in about one and a half minutes. I coded a further routine that checked that for every email the date matched the name of a jpg file. I could include this routine in my answer if you would like to perform the same check.

Related

Detect new mail then extract, unzip and rename attachments

I receive 4 weekly emails from 3 different senders.
Emails 1 and 2 are from the same sender and can be recognized through VBA. These emails contain zip files, where each zip file has one .csv file.
Emails 3 and 4 can also be recognized by VBA and the attachments are Excel sheets (.xlsx).
I want to extract and unzip (where needed) and save these 4 files in a folder as; email1.report, email2.report etc.
Then make a copy of these 4 files in a different folder for each file and rename like; "Today's date".email1.report.csv etc.
I want to combine these steps in a single code and to replace the email1.report, email2.report etc., files without a prompt asking "do you want to replace the files? Yes, No?"
Is it possible to detect the new weekly emails and do this automatically?
The code I use to unzip and save:
Else
For Each Atmt In Item.Attachments
If Right(Atmt.FileName, 3) = "zip" Then
FileNameFolder = "C:\Users\..."
FileName = FileNameFolder & Left(Atmt.FileName, (InStr(1, Atmt.FileName, ".zip") - 1)) & ".txt"
Atmt.SaveAsFile FileName
FileNameT = FileNameFolder & Atmt.FileName
Name FileName As FileNameT
Set oApp = CreateObject("Shell.Application")
oApp.NameSpace((FileNameFolder)).CopyHere oApp.NameSpace((FileNameT)).Items
Kill FileNameT
i = i + 1
End If
Next Atmt
'item.Close
End If
I won't develop the code for your specific problem, but I recently wrote something similar. Maybe you can go from here by altering to your criteria etc.
In my case I had two e-mails incoming shortly after another, within 60 seconds. Both mails had "FP" in their subject and a .pdf-attachment. The task was to concatenate these attachments using the installed PDF24, which luckily offers a shell command for this.
This was the code, placed in the "ThisOutlookSession" of the Outlook VBA project explorer.
Public btAttachmentMails As Byte
Public dtArrivalStamp As Date
Public strPathFirstMailAttachment As String
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)
On Error GoTo ErrorHandler
Dim Msg As Outlook.MailItem
If TypeName(Item) = "MailItem" Then
Dim i As Integer
Dim strDocumentsFolder As String
strDocumentsFolder = CreateObject("WScript.Shell").SpecialFolders(16)
strPathFirstMailAttachment = strDocumentsFolder & "\attachment_mail1.pdf"
If Item.Subject Like "FP*" Then
If btAttachmentMails = 0 Then
'first mail -> save attachment and set counter to 1
btAttachmentMails = 1
dtArrivalStamp = Time
For i = 1 To Item.Attachments.Count
If InStr(Item.Attachments.Item(i).DisplayName, ".PDF") > 0 Then
Item.Attachments.Item(i).SaveAsFile strPathFirstMailAttachment
End If
Next i
ElseIf btAttachmentMails = 1 Then
Dim dtNow As Date: dtNow = Time
If TimeDiff(dtArrivalStamp, dtNow) <= 60 Then
'second mail within 60 seconds with subject containing "FP" -> save attachment and concatenate both via pdf24, then delete both files
'save attachment of second mail
Dim strPathSecondMailAttachment As String
strPathSecondMailAttachment = strDocumentsFolder & "\attachment_mail2.pdf"
For i = 1 To Item.Attachments.Count
If InStr(Item.Attachments.Item(i).DisplayName, ".PDF") > 0 Then
Item.Attachments.Item(i).SaveAsFile strPathSecondMailAttachment
End If
Next i
'concatenate pdf documents via pdf24 shell
Dim strOutputPath As String
strOutputPath = CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\" & Year(Date) & Month(Date) & Day(Date) & "_Wartungsplan_" & Replace(CStr(Time), ":", "-") & ".PDF"
Shell ("""C:\Program Files (x86)\PDF24\pdf24-DocTool.exe"" -join -profile ""default/good"" -outputFile " & strOutputPath & " " & strPathFirstMailAttachment & " " & strPathSecondMailAttachment)
'inform user
MsgBox ("Files have been successfully concatenated. You can find the combined file on your desktop.")
'reset status, delete temporary documents
btAttachmentMails = 0
If CreateObject("Scripting.FileSystemObject").fileexists(strPathFirstMailAttachment) Then Kill strPathFirstMailAttachment
If CreateObject("Scripting.FileSystemObject").fileexists(strPathSecondMailAttachment) Then Kill strPathSecondMailAttachment
Else
'second mail did not arrive within 60 seconds -> treat as first mail
'save new arrival time and overwrite old firstMailAttachment with this one
dtArrivalStamp = Time
For i = 1 To Item.Attachments.Count
If InStr(Item.Attachments.Item(i).DisplayName, ".PDF") > 0 Then
Item.Attachments.Item(i).SaveAsFile strPathFirstMailAttachment 'overwrites existing file
End If
Next i
End If
End If
End If
End If
ExitNewItem:
Exit Sub
ErrorHandler:
MsgBox Err.Number & " - " & Err.Description & " - please contact XY"
Resume ExitNewItem
End Sub
Function TimeDiff(StartTime As Date, StopTime As Date)
TimeDiff = Abs(StopTime - StartTime) * 86400
End Function
cr44sh has posted an answer while I was creating mine. He has recommended using a new item event while I have recommended using a rule. I prefer rules but you can choose which ever approach you favour.
It is impossible to fully answer your question but I believe I can give enough help for you to construct the macros you need yourself.
You say that these emails can be identified with VBA. That suggests the best approach is an Outlook rule which uses the “run a script” option where “run a script” means “run a macro”. I will discuss the rule later but first you need the macros that will be run.
You will need two macros like this:
Public Sub Type1Email(ByRef ItemCrnt As MailItem)
' Relevant code
End Sub
Public Sub Type2Email(ByRef ItemCrnt As MailItem)
' Relevant code
End Sub
I am sure you can create better names for these macros. I have read that macros to be run by a rule must be in ThisOutlookSession. In my experience, they can be in an ordinary module providing they are declared as Public. I only use ThisOutlookSession for code that has to be in that code area. If code can be in a module, that is where I place it. I suggest creating a new module which will be named Module1 or Module2. Use function key F4 to access its properties and rename it as “ModRuleMacros” or similar. Giving modules meaningful names makes it so much easier to find the code you want to look at today.
Although the aim is to create a macro to be run by a rule, you need a way of testing the macro. If you have some of these emails saved somewhere, you can activate the rule by moving one of those emails to Inbox. However, I generally find it easier to use a macro like this:
Sub TestType1Email()
Dim Exp As Explorer
Dim ItemCrnt As MailItem
Set Exp = Outlook.Application.ActiveExplorer
If Exp.Selection.Count = 0 Then
Call MsgBox("Pleaase select one or more emails then try again", vbOKOnly)
Exit Sub
Else
For Each ItemCrnt In Exp.Selection
Call Type1Email(ItemCrnt)
Next
End If
End Sub
To use this macro, you select one or more Type1 emails and then run macro TestType1Email. This macro will pass the selected emails, one at a time, to the macro Type1Email. This will allow you to single step through macro Type1Email and ensure that it works to your entire satisfaction. I find this to be the easier method of testing a new Outlook macro.
It may be helpful to check what a rule can do for you. Select one of these emails and then click on Rules, which is in the middle of the Home tab, and then Create rule …. Selecting one of these emails means the first window is filled out with some options. Click Advanced options …. The new window lists all the options for selecting an email. Are all the options you need to select a type 1 or a type 2 email listed? The list is comprehensive but not complete. For example, you cannot select by the presence of attachments. Identify the options you can use and identify the options you need that are missing. Click Cancel twice to exist from rule creation.
You will need include code for any missing options in your macro.
Your question implies you have all the code you need for processing the emails except for suppressing the replace question. You need to check if there is an existing file before creating the new file. This is the routine that I use to check if a file exists:
Public Function FileExists(ByVal PathName As String, ByVal FileName As String) As Boolean
' Returns True if file exists. Assumes path already tested.
' Coded by Tony Dallimore
' Based on code written by iDevlop: http://stackoverflow.com/a/28237845/973283
' Ensure only one "\" between path and filename
If Right$(PathName, 1) <> "\" Then
PathName = PathName & "\"
End If
If Left$(FileName, 1) = "\" Then
FileName = Mid$(FileName, 2)
End If
FileExists = False
On Error Resume Next
FileExists = ((GetAttr(PathName & FileName) And vbDirectory) <> vbDirectory)
On Error GoTo 0
End Function
If the file exists, you can:
Use VBA statement Kill (https://learn.microsoft.com/en-gb/office/vba/Language/Reference/user-interface-help/kill-statement) to delete the old file.
Use VBA statement Name (https://learn.microsoft.com/en-gb/office/vba/language/reference/user-interface-help/name-statement) to move the old file to another folder or rename it perhaps by adding a date at the beginning of the name.
I favour the second option because I do not like deleting a file until I am really, really sure I will not need it again. I saw too many situations during my career where a file deleted as no longer needed was found to be incorrectly or incompletely processed a few months later.
Once you have fully tested the macros, you can create the rules to execute them. For each type of email:
Select an email of the required type.
Click on Rules and then Create rule ….
Tick any relevant boxes on the first window.
Click Advanced options ….
Tick all relevant boxes on the second window.
Click Next.
Tick the box against “Run a script”.
Click a script.
You will be shown a list of all the macros that can be run from a rule. Select the required macro.
Click Next.
Tick the box against any appropriate exceptions and enter any additional information required.
Click Next.
Name the rule. Tick “run this rule against any messages already in Inbox” if required. Review the rule and edit if necessary.
Click Finish.
I hope the above is enough to plug the holes in your knowledge.

My macro saves PDF attachment from one sender/subject. How get it to handle multiple sender/subjects?

I have a code that can automaticaly move a PDF from a received message to a folder of my choice, but what I really need is in fact to be able to move a file to a specific folder depending of the sender.
The code below works for only one sender, How do I add more senders and more folder locations?
Private WithEvents Items As Outlook.Items
Private Sub Application_Startup()
Dim olApp As Outlook.Application
Dim objNS As Outlook.NameSpace
Set olApp = Outlook.Application
Set objNS = olApp.GetNamespace("MAPI")
Set Items = objNS.GetDefaultFolder(olFolderInbox).Items
End Sub
Private Sub Items_ItemAdd(ByVal item As Object)
On Error GoTo ErrorHandler
'Only act if it's a MailItem
Dim Msg As Outlook.MailItem
If TypeName(item) = "MailItem" Then
Set Msg = item
'Change variables to match need. Comment or delete any part unnecessary.
If (Msg.SenderName = "Marc, Test") And _
(Msg.Subject = "Heures") And _
(Msg.Attachments.Count >= 1) Then
'Set folder to save in.
Dim olDestFldr As Outlook.MAPIFolder
Dim myAttachments As Outlook.Attachments
Dim Att As String
'location to save in. Can be root drive or mapped network drive.
Const attPath As String = "C:\Users\NAEC02\Test\"
' save attachment
Set myAttachments = item.Attachments
Att = myAttachments.item(1).DisplayName
myAttachments.item(1).SaveAsFile attPath & Att
' mark as read
Msg.UnRead = False
End If
End If
ProgramExit:
Exit Sub
ErrorHandler:
MsgBox Err.Number & " - " & Err.Description
Resume ProgramExit
End Sub
Before answering your question, some comments on your existing code.
You are running this code within Outlook. You do not need olApp. You only need a reference to the Outlook application if you are trying to access your emails from Excel or some other Office product.
I am surprised how often I see On Error GoTo ErrorHandler because I have never found a use from this statement.
If I am coding for myself, I want execution to stop on the statement causing the problem so I can understand what is happening without guessing from the error message. If execution stops on the statement causing the error, I can restart the code if I can immediately fix the error.
If I am developing for a client, I want, at worst, a user-friendly message. Err.Number & " - " & Err.Description is not my idea of a user-friendly message. It does not even tell me which email caused the problem. For a client, I would have something like:
Dim ErrDesc as String
Dim ErrNum as Long
: : :
On Error Resume Next
Statement that might give an error
ErrNum = Err.Num
ErrDesc = Err.Description
On Error GoTo 0
If ErrNum <> 0 Then
Code to handle errors that can occur with
this statement in a user-friendly manner.
End If
Today Dim Att As String is fine because you remember what Att is. Will you remember when you update this macro in six or twelve months? Will a colleague updating this macro know what Att is? I would call it AttName or perhaps AttDsplName.
You say the code saves PDF attachments but you do not check for this. To a VBA macro, logos, images, signatures and other files are also attachments. Also you assume the attachment you wish to save is Attachments(1). If there are several attachments, the logos, images and signatures could come first.
You have:
'Set folder to save in.
Dim olDestFldr As Outlook.MAPIFolder
You do not set olDestFldr and you do not move the email to a different folder. Do you want to do this?
Now to your question. I have included the code for two methods of achieving your objective and I discuss another two methods. However, before showing you the code, I suspect I need to introduce you to Variants. Consider:
Dim A As Long
Dim B As String
Dim C As Double
Dim D As Variant
I have declared A to C as a long integer, a string and a double. These variables can never be anything else and must be used in accordance with the rules for their type. I can write A = A + 1 or A = A * 5. Providing the new value for A does not exceed the maximum value for a long integer, these statements are fine. But I cannot write A = "House" because "House" is not an integer. I can write B = "House" because "House" is a string. I can write B = "5" and then A = A + B because VBA will perform implicit conversions if it can. That is, VBA can convert string "5" to integer 5 and add it to A.
I can also write:
D = 5
D = D + A
D = "House"
D is a Variant which means it can hold any type of data. Here I assign 5 to D then add A so for these two statements, D is holding an integer. I then change my mind and assign a string to D. This is not very sensible code but it is valid code. D can hold much more than an integer and a string. In particular, it can hold an array. Consider:
ReDim D(0 To 2)
D(0) = "House"
D(1) = A + 5
D(2) = 3.7
Following the ReDim statement, it is as though D has been converted to an array and I use array syntax to access the elements of D. D(0) contains "House", D(1) contains 5 more than the current value of A and D(2) contains double 3.7.
I can achieve the same effect with:
D = Array("House", A + 5, 3.7)
I am sure you agree this is easier. Array is a function that can take a large number of parameters and returns a Variant array containing those parameters which I have assigned to D. I do not normally advise mixing types within a variant array since it is very easy to get yourself into a muddle. However, it is valid VBA and I have found it invaluable with particularly difficult problems. Normally, I would not use function Array, I would write:
D = VBA.Array("House", A + 5, 3.7)
With VBA.Array, the lower bound of the array is guaranteed to be zero. With Array, the lower bound depends on the Option Base statement. I have never seen anyone use the Option Base statement, but I do not like to risk having my code changed by someone adding this statement. Search for “VBA Option Base statement” to discover what this statement does.
The following code demonstrates my first method of achieving your objective:
Option Explicit
Sub Method1()
Dim DiscFldrCrnt As Variant
Dim DiscFldrs As Variant
Dim Inx As Long
Dim SenderNameCrnt As Variant
Dim SenderNames As Variant
Dim SubjectCrnt As Variant
Dim Subjects As Variant
SenderNames = VBA.Array("Doe, John", "Early, Jane", "Friday, Mary")
Subjects = VBA.Array("John's topic", "Jane's topic", "Mary's topic")
DiscFldrs = VBA.Array("DoeJohn", "EarlyJane", "FridayMary")
For Inx = 0 To UBound(SenderNames)
SenderNameCrnt = SenderNames(Inx)
SubjectCrnt = Subjects(Inx)
DiscFldrCrnt = DiscFldrs(Inx)
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
Next
End Sub
If you copy this code to a module, you can run it and see what it does. If you work slowly through it, you should be able to understand what it is doing. Come back with questions if necessary but the more you can discover for yourself, the faster you will develop your own skills.
Note: the disc folders have names such as “DoeJohn”. I am assuming you would have something like "C:\Users\NAEC02\Test\" as a root folder and you would save the attachment to "C:\Users\NAEC02\Test\DoeJohn\".
I use this method when I have a small number of values I need to link. It relies on SenderNames(#), Subjects(#) and DiscFldrs(#) being associated. As the number of different combinations increase, it can be difficult to keep the three arrays in step. Method2 solves that problem.
Sub Method2()
Dim DiscFldrCrnt As Variant
Dim Inx As Long
Dim SenderNameCrnt As Variant
Dim SubjectCrnt As Variant
Dim TestValues As Variant
TestValues = Array("Doe, John", "John's topic", "John", _
"Early, Jane", "Jane's topic", "Jane", _
"Friday, Mary", "Mary's topic", "Mary")
For Inx = LBound(TestValues) To UBound(TestValues) Step 3
SenderNameCrnt = TestValues(Inx)
SubjectCrnt = TestValues(Inx + 1)
DiscFldrCrnt = TestValues(Inx + 2)
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
Next
End Sub
Here I have placed all the values in a single array. If I want to add a new sender, I add another three elements to the end of the array which I find this easier to manage. For the code to process the three values, Method1 and Method2 are identical.
The principle disadvantage of Method2 compared with Method1 is that the total number of values is reduced. I like to see all my code so I do not like statements that exceed the width of the screen. This limits my lines to about 100 characters. I use the continuation character to spread the statement over several lines but there is a maximum of 24 continuation lines per statement. With Method1, I am spreading the values over three arrays and therefore three statements so I can have three times as many values. In practice this is not a real limit. Both Method1 and Method2 become too difficult to manage before the VBA limits are reached.
The real disadvantage of Method1 and Method2 is that every change requires the services of a programmer. If user maintenance is important, I use Method3 which reads a text file into arrays or Method4 which reads from an Excel worksheet. I have not included code for either Method3 or Method4 but can add one or both if you need this functionality. I find most users prefer a worksheet but those with a favourite text editor prefer a text file.
In the middle of both Method1 and Method2 I have:
' Code to process SenderNameCrnt, SubjectCrnt and DiscFldrCrnt
Debug.Print SenderNameCrnt & " " & SubjectCrnt & " " & DiscFldrCrnt
You need to replace these statements with a variation of your existing code. I have no easy method of testing the following code so it is untested but it should give you are start.
This is a new version of Items_ItemAdd designed to work with either of my methods.
Private Sub Items_ItemAdd(ByVal Item As Object)
Const DiscFldrRoot As String = "C:\Users\NAEC02\Test\"
' * There is no need to write Outlook.MailItem because (1) you are within Outlook
' and (2) there is no other type of MailItem. You only need to specify Outlook
' for folders since there are both Outlook and Scripting folders. Note:
' "Scripting" is the name of the library containing routines for disc folders.
' * Do not spread your Dim statements throughout your sub. There are languages
' where you can declare variables within code blocks but VBA is not one of those
' languages. With VBA, you can declare variables for an entire sub or function,
' for an entire module or for an entire workbook. If you spread your Dim
' statements out it just makes them hard to find and you are still declaring
' them at the module level.
Dim DiscFldrCrnt As Variant
Dim InxA As Long
Dim Msg As MailItem
Dim SenderNameCrnt As Variant
Dim SubjectCrnt As Variant
' You also need the arrays from whichever of Method1 or Method2 you have chosen
If TypeName(item) = "MailItem" Then
' Only interested in MailItems
Set Msg = Item
' Code from Method1 or Method2 with the code below in the middle
End If
End Sub
Insert the body of Method1 or Method2, whichever you chose, in the middle of the above code. Then insert the following code in the middle of that code.
With Msg
If .Attachments.Count = 0 Then
' Don't bother to check MailItem if there are no attachments
Else
If .Subject <> SubjectCrnt Then
' Wrong subject so ignore this MailItem
ElseIf .SenderName <> SenderNameCrnt Then
' Wrong sender name so ignore this MailItem
Else
' SenderName and Subject match so save any PDF attachments
For InxA = 1 to .Attachments.Count
If LCase(Right$(.Attachments(InxA).DisplayName, 4)) = ".pdf" Then
' Warning: SaveAsFile overwrites existing file with the same name
.Attachments(InxA).SaveAsFile DiscFldrRoot & DiscFldrCrnt & _
.Attachments(InxA).DisplayName
End If
End With
Next
End If
End With

VBA, MS Outlook, Folder Item

I want to implement an VBA application, which uses the selected object (E-mail, task, folder).
My try with Application.ActiveExplorer.Selection.Item(i_item) seems to return only mails, tasks, calender entries or notes but never an folder (e.g. 'Inbox\').
When the user selects an e-mail, and then starts the VBA macro, the solution Application.ActiveExplorer.Selection.Item(i_item) delivers the desired results.
However, if the last item picked by the Outlook user was an folder (e.g. 'Sent Mails'). And the VBA makro started afterward, than the macro should recive the Folder Item (without additional user interaction). This is currently not the case. The code above still delivers the e-mail, or task.
How do I check, if the last selection was on an folder (not an e-mail, etc)?
How do I access the Folder item?
If this is not possible I will switch back to Pickfolder (like proposd by Darren Bartrup-Cook) but this is not me prefred solution.
I want to get the selected folder in order to change its icon, so our code is somehow the same.
I noticed that Application.ActiveExplorer.Selection.Item(i_item) it is not perfect, since it throws an exception for empty folders or on calendar etc.
So I use Application.ActiveExplorer.CurrentFolder.DefaultMessageClass (Application.ActiveExplorer.NavigationPane.CurrentModule.Name or Application.ActiveExplorer.NavigationPane.CurrentModule.NavigationModuleType) in order to figure out where I actually am.
By that approach it is easy to get current selected folder
Dim folder As Outlook.MAPIFolder
Dim folderPath As String, currItemType As String
Dim i As Integer
currItemType = Application.ActiveExplorer.CurrentFolder.DefaultMessageClass
If currItemType = "IPM.Note" Then 'mail Item types https://msdn.microsoft.com/en-us/library/office/ff861573.aspx
Set folder = Application.ActiveExplorer.CurrentFolder
folderPath = folder.Name
Do Until folder.Parent = "Mapi"
Set folder = folder.Parent
folderPath = folder.Name & "\" & folderPath
Loop
Debug.Print folderPath
End If
haven't got an problem with it yet. In your case, you can store the selection in a global variable, so you always know which folder was selected last.
This procedure will ask you to select the folder.
If you interrupt the code and examine the mFolderSelected or MySelectedFolder then you should be able to work something out:
Public Sub Test()
Dim MySelectedFolder As Variant
Set MySelectedFolder = PickFolder
End Sub
Public Function PickFolder() As Object
Dim oOutlook As Object 'Outlook.Application
Dim nNameSpace As Object 'Outlook.Namespace
Dim mFolderSelected As Object 'Outlook.MAPIFolder
On Error GoTo ERROR_HANDLER
Set oOutlook = CreateObject("Outlook.Application")
Set nNameSpace = oOutlook.GetNameSpace("MAPI")
Set mFolderSelected = nNameSpace.PickFolder
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'The commented out code will return only email folders. '
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
If Not mFolderSelected Is Nothing Then
' If mFolderSelected.DefaultItemType = 0 Then
Set PickFolder = mFolderSelected
' Else
' Set PickFolder = Nothing
' End If
Else
Set PickFolder = Nothing
End If
Set nNameSpace = Nothing
Set oOutlook = Nothing
On Error GoTo 0
Exit Function
ERROR_HANDLER:
Select Case Err.Number
Case Else
MsgBox "Error " & Err.Number & vbCr & _
" (" & Err.Description & ") in procedure PickFolder."
Err.Clear
End Select
End Function
NB: This was written to be used in Excel and has late binding - you'll need to update it to work in Outlook (no need to reference Outlook for a start).

Get the folder where the last mailitem was moved in Outlook?

I have a vbscript macro that I'm using in Outlook. It moves a mailitem to some folder, say X. After I run the macro and I try to manually move a mailitem from Outlook with Control-v, it defaults to folder X. I would like Control-v to default to the folder that it would have used before I ran the macro.
Is there some way in VBScript to find out what folder the last mailitem was move to, and to return that to be the default folder after I run my script? Or is there a way to move a mailitem in my script without the destination folder being remembered by Outlook Control-v after I run the script?
Thanks for any hints.
OK, here is the code I'm using. It is a macro to save a mailitem as HTML and open it in a browser. I save any attachments in a separate directory and I add in a list of URLs to the attachments. I do this by modifying the mailitem, but I don't want change the original message - I want it to remain in my inbox as it was. So I create a copy and when I'm done I want to get rid of the copy. For some reason the .Delete method just doesn't do anything. So, one solution for me would be to figure out why .Delete is not working. I created a work-around by just moving the copied message into my deleted items folder. The problem I have with this is that I often use control-v to move items from my inbox to an archive folder. Once I run the macro, though, the default folder for control-v is the deleted item folder. I keep archiving items there by mistake. So the best solution would be to get .Delete working, but even then, that might change the control-v default behavior after running the macro.
Here's the code. I've only been doing vba for a couple of days, so any tips on things I'm missing appreciated.
Option Explicit
Sub CreateHTML()
Select Case TypeName(Outlook.Application.ActiveWindow)
Case "Inspector"
CreateHTMLfromObject Outlook.Application.ActiveInspector.CurrentItem
Case "Explorer"
Dim objItem As Object
For Each objItem In Outlook.Application.ActiveExplorer.Selection
CreateHTMLfromObject objItem
Next
End Select
End Sub
Sub CreateHTMLfromObject(objItem As Object)
' For now, assume all items are mail items
'Select Case objItem.Class
'Case olMail
Dim objMailOrig As MailItem
Dim objMailCopy As MailItem ' Work on a copy of the message
Set objMailOrig = objItem
Set objMailCopy = objMailOrig.copy
' Where all HTML versions of messages will be stored
Dim fileDir As String
fileDir = "C:\Lib\olHTML\"
' A unique message id from the original message
Dim MsgId As String
MsgId = objMailOrig.EntryID
' The file the HTML version of the message will be stored in
Dim fileName As String
fileName = MsgId & ".html"
' The full file system path where the HTML verison of the message will be stored
Dim filePath As String
filePath = fileDir & fileName
' ---------------------------------------------------------------
' Save Attachments
' ---------------------------------------------------------------
' Subdirectory for attachments on this message
' A unique subdirectory for each message
Dim atmtDir As String
atmtDir = MsgId & "_atmt\"
' Full file system path to the attachment directory
Dim atmtDirPath As String
atmtDirPath = fileDir & atmtDir
' File system object for creating the attachment folder
Dim oFSO
Set oFSO = CreateObject("Scripting.FileSystemObject")
If (objMailCopy.Attachments.Count > 0) And (Not oFSO.FolderExists(atmtDirPath)) Then
oFSO.CreateFolder (atmtDirPath)
End If
' To hold the full file system path to each attachment file
Dim atmtFilePath As String
' String to accumulate HTML code for displaying links to attachments
' in the body of the HTML message
Dim atmtLinks As String
atmtLinks = " "
Dim atmt As Attachment
For Each atmt In objMailCopy.Attachments
atmtFilePath = atmtDirPath & atmt.fileName
atmt.SaveAsFile atmtFilePath
' create a relative URL
atmtLinks = atmtLinks & _
"<br><a href='" & atmtDir & atmt.fileName & "'>" & atmt.fileName & "</a>"
Next atmt
' ---------------------------------------------------------------
' Add links to attachments
' ---------------------------------------------------------------
' This changes the original message in Outlook - so we work on a copy
' Convert body to HTML if RTF, Text or other format
If (objMailCopy.BodyFormat = olFormatPlain Or olFormatRichText Or olFormatUnspecified) Then
objMailCopy.BodyFormat = olFormatHTML
End If
' Add attachments links at the beginning
If objMailCopy.Attachments.Count > 0 Then
objMailCopy.HTMLBody = _
"<p>" & "Attachments: " & atmtLinks & "</p>" & objMailCopy.HTMLBody
End If
' ---------------------------------------------------------------
' Save the HTML message file
' ---------------------------------------------------------------
objMailCopy.SaveAs filePath, olHTML
' ---------------------------------------------------------------
' Delete the copy from Outlook
' ---------------------------------------------------------------
'! This seems to have no effect
' objMailCopy.Delete
' Move copied message to deleted items folder
objMailCopy.Move Outlook.Application.GetNamespace("MAPI").GetDefaultFolder(olFolderDeletedItems)
' ---------------------------------------------------------------
' Open the HTML file with default browser
' ---------------------------------------------------------------
Dim url As String
url = "file:///" & filePath
CreateObject("WScript.Shell").Run (url)
End Sub
i would not make a copy in the inbox and delete that afterwards (that will make your deleted-folder explode one day), but make your changes in the local copy of the message-file:
here an example:
Sub changelocalcopy(olitem As Outlook.MailItem)
Dim oNamespace As Outlook.NameSpace
Set oNamespace = Application.GetNamespace("MAPI")
Dim oSharedItem As Outlook.MailItem
Dim pfaddatei As String
pfaddatei = c:\test.msg 'path for your local copy here
olitem.SaveAsFile pfaddatei
Set oSharedItem = oNamespace.OpenSharedItem(pfaddatei)
'now do your changes
'you will not want the following line, I leave it here in case you Need it:
Kill pfaddatei
oSharedItem.Close (olDiscard)
Set oSharedItem = Nothing
Set oNamespace = Nothing
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