Friday, June 25, 2004

Recover Deleted Items from the Dumpster Programmatically

I started looking at this because I thought it would be a good way of finding any spam that was getting past my IMF filter that users weren't reporting. So I went of and hit the ESDK and CDO doco for any clues also did a Google crawl and didn't really turn up much except that CDO was not going to do the trick. The next step was to look at OWA and see if I could reuse any of the stuff it uses. After examining some of the conversations my browser was having with OWA I found that OWA was issuing a query with a scope of softdeleted eg "from scope ('SOFTDELETED traversal of ""') . I Googled this which turned up this Blog post from KC which started to fill in a few of the blanks.

The next thing I tried to do was use a Softdeleted traversal in a Exoledb script which although it seemed to be a valid query it didn't work or more to the point the query worked but instead of returning a record set of soft deleted items I just got all normal folder items. I tried the same thing again instead this time using MSDAIPP.dso (which uses WebDAV) and this worked so it seemed that the Softdeleted traversal only works in WebDAV and not Exoledb.

By this time I was quite interested and started thinking that I could go a bit further then just reporting on missed SPAM so I started looking at ways I could maybe copy this data out into a central repository. I started hitting a few problems because although I could access these soft deleted emails as a recordset, I couldn't open a record against any one item because as far as the mail API's where concerned the resource at that URL had been deleted. This lead me back to the OWA conversations I was looking at and I noticed when OWA restored an item from the dumpster it used the /-softdeleted-/-FlatUrlSpace-/ namespace to copy the item back into the store. I tried to access the /-softdeleted-/-FlatUrlSpace-/ directly with a ADO record but this still didn't work so I resigned to just duplicating OWA functionality which was to do a WebDav Bcopy of the items in the dumpster and then BDelete the items from the dumpster using the /-softdeleted-/-FlatUrlSpace-/ to reference the source items. The last problem was to work out what the /-softdeleted-/-FlatUrlSpace-/ property of the email was as this didn't seem to be located in the recordset either, what I found was that the /-softdeleted-/-FlatUrlSpace-/ property was the same as the /-FlatUrlSpace-/ which is available as the http://schemas.microsoft.com/exchange/permanenturl property all I needed was to append (foldername in my case deleteded items)/-softdeleted-/ before /-FlatUrlSpace-/

So what I ended up with is a script that will copy all the items from the Dumpster using a Bcopy into a folder in the users inbox and then deletes all the items from the dumpster. Its important to do the deletion because if you don't you'll end up with duplicates when the user deletes the emails again.


sDestinationURL = "http://yourserver/exchange/yourmailbox/test/"
Set XMLreq = CreateObject("Microsoft.xmlhttp")
XMLreq.open "BCOPY", "http://yourserver/exchange/yourmailbox/inbox/", False
XMLreq.setRequestHeader "Destination", sDestinationURL
xmlstr = "<?xml version=""1.0"" ?>"
xmlstr = xmlstr & "<D:copy xmlns:D=""DAV:"">"
Set Rec = CreateObject("ADODB.Record")
Set Rs = CreateObject("ADODB.Recordset")
rec.open "http://yourserver/exchange/yourmailbox", ,3
inbstr = "http://yourserver/exchange/yourmailbox/deleted items/"
strView = "select * from scope ('SOFTDELETED traversal of """ & inbstr & """') "
Rs.Open strView, rec.activeconnection, 3
If Rs.RecordCount <> 0 Then
while not rs.eof
xmlstr = xmlstr & "<D:target>"
xmlstr = xmlstr & "<D:href>" & replace(rs.fields("http://schemas.microsoft.com/exchange/permanenturl")" _
& ","/-FlatUrlSpace-/","/Deleted Items/-softdeleted-/-FlatUrlSpace-/") & "</D:href>"
xmlstr1 = xmlstr1 & "<D:href>" & replace(rs.fields("http://schemas.microsoft.com/exchange/permanenturl")" _
& ","/-FlatUrlSpace-/","/Deleted Items/-softdeleted-/-FlatUrlSpace-/") & "</D:href>"
xmlstr = xmlstr & "<D:dest>" & replace(rs.fields("Dav:href"),"Deleted%20Items","targetfolder") & "</D:dest>"
xmlstr = xmlstr & "</D:target>"
rs.movenext
wend
End If
xmlstr = xmlstr & "</D:copy>"
XMLreq.setRequestHeader "Content-Type", "text/xml;"
XMLreq.setRequestHeader "Translate", "f"
XMLreq.setRequestHeader "Content-Length:", Len(xmlstr)
XMLreq.send(xmlstr)
If (XMLreq.Status >= 200 And XMLreq.Status < 300) Then
Wscript.echo "Success! " & "Results = " & XMLreq.Status & ": " & XMLreq.statusText
ElseIf XMLreq.Status = 401 then
Wscript.echo "You don't have permission to do the job! Please check your permissions on this item."
Else
Wscript.echo "Request Failed. Results = " & XMLreq.Status & ": " & XMLreq.statusText
End If
XMLreq.open "BDELETE", "http://yourserver/exchange/yourmailbox/Deleted Items/", False
xmlstr = "<?xml version=""1.0"" ?>"
xmlstr = xmlstr & "<D:delete xmlns:D=""DAV:"">"
xmlstr = xmlstr & "<D:target>"
xmlstr = xmlstr & xmlstr1
xmlstr = xmlstr & "</D:target>"
xmlstr = xmlstr & "</D:delete>"
XMLreq.setRequestHeader "Content-Type", "text/xml;"
XMLreq.setRequestHeader "Translate", "f"
XMLreq.setRequestHeader "Content-Length:", Len(xmlstr)
XMLreq.send(xmlstr)
If (XMLreq.Status >= 200 And XMLreq.Status < 300) Then
Wscript.echo "Success! " & "Results = " & XMLreq.Status & ": " & XMLreq.statusText
ElseIf XMLreq.Status = 401 then
Wscript.echo "You don't have permission to do the job! Please check your permissions on this item."
Else
Wscript.echo "Request Failed. Results = " & XMLreq.Status & ": " & XMLreq.statusText
End If

10 comments:

Tim In Jax said...

Glen,

You are the man. I have been trying to find a way to accomplish this task for a while now and all I came up with was deadends. There are many resources out there about using C++ amd extended MAPI but none about webDAV. Thanks for all of your hardwork and effort and sharing with the rest of us.

Teo Heras said...

Glen,

Could this be used to pull items from the dumpster that were 'shift deleted' from the inbox as well? If so, I have a follow up question. Is it possible to redirect the items to another mailbox. The legal & HR departments have made a few requests to pull mail from a users dumpster without (him/her) knowing. Exmerge doesn't always work, so we've had to do recovery's and then connect a new AD account in order to be able to export the mail.

Glen said...

Yes Every folder has its own dumpster. Doing a shift delete means that all your doing is moving that item into the dumpster of the folder your deleting it in instead of the deleted items folder. So you could write a script that did a export of mail that was deleted (shift-deleted) from the Inbox, Sent Items etc using the same method (as long as you have retention configured). The users themselves would not be able to tell your doing this. You can also do this from Outlook have a look at http://support.microsoft.com/kb/246153

Anonymous said...

I am trying to recover from some issues with POP3 accounts on and Exchange 2003 server. I tried the script in your blog but using cscript to call it and I getting compile errors in line 16 "expected ')'. Any ideas?

Anonymous said...

Fixed the problem...had to remove the _Return piece and drop some quotes...Thanks,

Vikas Verma said...

Hey Glen,

Thanks for the sample. It helped me a lot, but trust me it did not worked for me as it is. I had created a C# sample for the job, in case anyone need it.

http://blogs.msdn.com/vikas/archive/2008/04/28/howto-webdav-ews-programmatically-recover-soft-deleted-items-from-dumpster.aspx

Thanks once again, you save me some time... Cheers :)

~Vikas

Anonymous said...

Will this work for 2007?
We are trying to find out who is deleting items from a public folder. The Tools -> Recover Deleted Items from (folder)... does not give you that information, and once it has been recovered, the "chaged by" field displays the name of the person who recovered it, not who deleted it. Please help...

Glen said...

I think this will work on 2007 have a look at http://gsexdev.blogspot.com/2005/11/displaying-deleted-public-folder.html

Anonymous said...

Hi Glen,
How would you authenticate with the server?

Shekhar said...

Hi Glen,

I need to run this script for Ex 2007,
Could you please guide me how to run this and what are the input parameters...