Share via


HOWTO: WebDAV: EWS: Programmatically recover soft deleted items from dumpster

As you may already know, EWS has support for Soft-Deleting items but the irony is it cannot access the soft-deleted items. This is a design challenge and product group is aware of this limitation. Hopefully we should see this support natively in EWS sooner or later. If you really need a solution or as we term it as "workaround" to the problem, you can still use WebDAV or MAPI to recover them. MAPI is very powerful but cant work from managed code.

I had faced little tough time to decode the way it works, ofcourse Glen's blog was very helpful, but unfortunately it did not work as expected and I had to create my own code. 

Disclaimer: This is just a sample code to serve as proof of concept, I would recommend to use MAPI

See the below KB for MAPI sample code to do the job

https://support.microsoft.com/kb/232265

 

public static void WebDAV_RecoverDeletedFolder(string hostname, string user,string pass,string sourceFolder, string targetFolder)
        {

            Console.WriteLine("Info: Searching for \"Soft Deleted\" items in \"" + sourceFolder + "\", will move them to \"" + targetFolder + "\"");

            sourceFolder = sourceFolder.Replace(" ", "%20").ToLower();
            targetFolder = targetFolder.Replace(" ", "%20").ToLower();
            int itemCount = 0;

            string sourceForlderURL = "https://" + hostname + "/exchange/" + user + "/" + sourceFolder;

            #region Search all messages and generate the XML to copy them to new folder
            HttpWebRequest xmlReq = (HttpWebRequest)System.Net.WebRequest.Create(sourceForlderURL);

            StringBuilder strXml = new StringBuilder();
            StringBuilder strXmlForDeletion = new StringBuilder();

            strXml.Append("<?xml version='1.0'?>");
            strXml.Append("<D:searchrequest xmlns:D='DAV:'>");
            strXml.Append("<D:sql>SELECT ");
            strXml.Append("\"DAV:href\"");
            strXml.Append("FROM SCOPE ('SOFTDELETED TRAVERSAL OF \"" + sourceForlderURL + "\"')");
            strXml.Append("</D:sql></D:searchrequest>");

            xmlReq.Credentials = new System.Net.NetworkCredential(user, pass);
            xmlReq.AllowAutoRedirect = true;
            xmlReq.Method = "SEARCH";
            xmlReq.Headers.Add("Translate", "f");
            xmlReq.ContentType = "text/xml";
            xmlReq.ContentLength = strXml.Length;

            StreamWriter sw = new StreamWriter(xmlReq.GetRequestStream());
            sw.Write(strXml.ToString());
            sw.Flush();
            sw.Close();

            strXml.Remove(0, strXml.Length);

            string strOut = string.Empty;
            HttpWebResponse httpResp = (HttpWebResponse)xmlReq.GetResponse();
            if ((int)httpResp.StatusCode > 200 && (int)httpResp.StatusCode < 300)
                strOut = new StreamReader(httpResp.GetResponseStream()).ReadToEnd();
            else
                Console.WriteLine("Error: " + httpResp.StatusDescription + " (" + httpResp.StatusCode + "), while searching for deleted items");

            XmlDocument xDoc = new XmlDocument();
            xDoc.LoadXml(strOut);
            XmlNodeList xNodeList = xDoc.GetElementsByTagName("a:href");

            strXml.Append("<?xml version=\"1.0\" ?>");
            strXml.Append("<D:copy xmlns:D=\"DAV:\">");

            itemCount = xNodeList.Count / 2;
            Console.WriteLine("Info: " + itemCount + " deleted items found to recover");

            if (itemCount <= 0)
                return;

            for (int nCtr = 0; nCtr < xNodeList.Count - 1; nCtr += 2)
            {
                strXml.Append("<D:target>");
                strXml.Append("<D:href>" + xNodeList[nCtr].InnerText + "</D:href>");
                strXml.Append("<D:dest>" + xNodeList[nCtr + 1].InnerText.ToLower().Replace(sourceFolder, targetFolder) + "</D:dest>");
                strXml.Append("</D:target>");

                //We will use this XML later for deletion of these items
                strXmlForDeletion.Append("<D:href>" + xNodeList[nCtr].InnerText + "</D:href>");

            }
            strXml.Append("</D:copy>");

            #endregion

            #region Start Copying all the messages to target folder

            xmlReq = (HttpWebRequest)System.Net.WebRequest.Create(sourceForlderURL);
            xmlReq.Credentials = new System.Net.NetworkCredential(user, pass);
            xmlReq.AllowAutoRedirect = true;
            xmlReq.Method = "BCOPY";
            xmlReq.Headers.Add("Translate", "f");
            xmlReq.ContentType = "text/xml";
            xmlReq.ContentLength = strXml.Length;

            sw = new StreamWriter(xmlReq.GetRequestStream());
            sw.Write(strXml.ToString());
            sw.Flush();
            sw.Close();

            strOut = string.Empty;
            httpResp = (HttpWebResponse)xmlReq.GetResponse();
            if ((int)httpResp.StatusCode > 200 && (int)httpResp.StatusCode < 300)
            {
                strOut = new StreamReader(httpResp.GetResponseStream()).ReadToEnd();
                Console.WriteLine("Success: " + itemCount + " Items copied to target folder");
            }
            else
                Console.WriteLine("Error: " + httpResp.StatusDescription + " (" + httpResp.StatusCode + "), while copying deleted items to target folder");

            #endregion

            #region Delete all the messages from dumpster, to avoid duplication on next run

            strXml.Remove(0, strXml.Length);

            Console.WriteLine("Info: Deleting " + itemCount + " items from dumpster");

            strXml.Append("<?xml version=\"1.0\" ?>");
            strXml.Append("<D:delete xmlns:D=\"DAV:\">");
            strXml.Append("<D:target>");
            strXml.Append(strXmlForDeletion.ToString());
            strXml.Append("</D:target>");
            strXml.Append("</D:delete>");

            xmlReq = (HttpWebRequest)System.Net.WebRequest.Create(sourceForlderURL);
            xmlReq.Credentials = new System.Net.NetworkCredential(user, pass);
            xmlReq.AllowAutoRedirect = true;
            xmlReq.Method = "BDELETE";
            xmlReq.Headers.Add("Translate", "f");
            xmlReq.ContentType = "text/xml";
            xmlReq.ContentLength = strXml.Length;

            sw = new StreamWriter(xmlReq.GetRequestStream());
            sw.Write(strXml.ToString());
            sw.Flush();
            sw.Close();

            strXml.Remove(0, strXml.Length);

            strOut = string.Empty;
            httpResp = (HttpWebResponse)xmlReq.GetResponse();
            if ((int)httpResp.StatusCode > 200 && (int)httpResp.StatusCode < 300)
            {
                strOut = new StreamReader(httpResp.GetResponseStream()).ReadToEnd();
                Console.WriteLine("Success: " + itemCount + " Items deleted from dumpster");
            }
            else
                Console.WriteLine("Error: " + httpResp.StatusDescription + " (" + httpResp.StatusCode + "), while deleting items from dumpster");

            #endregion
        }

Comments