12

I have the following Inbox folder structure:

Inbox
--ABC
----ABC 2
----ABC 3
--XYZ
----XYZ 2
--123
----123 A
----123 B
----123 C

I am using Exchange Web Services and the following code to find the child folders of the Inbox folder:

ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010);

service.AutodiscoverUrl("MyName@MyDomain.com");
Mailbox mb = new Mailbox("MyName@MyDomain.com");

FindFoldersResults findResults = service.FindFolders(
    WellKnownFolderName.Inbox,
    new FolderView(int.MaxValue));

foreach (Folder folder in findResults.Folders)
{
    Console.WriteLine(folder.DisplayName);
}

This partly works because it returns the ABC, XYZ, and 123 folders; unfortunately, it does not return the folders inside each of those folders (ABC 2, ABC 3, XYZ 2, 123 A, 123 B, 123 C).

Also, it is possible that a folder could have more than one level of subfolders inside it.

How can I write this code so that it will return all subfolders regardless of how deeply nested they may be?

Sesame
  • 3,370
  • 18
  • 50
  • 75

2 Answers2

22

You can tell EWS to do a deep traversal when searching the folders. You can do this using the FolderView.Traversal property. Your code would then be changed to something similar to the following:

FindFoldersResults findResults = service.FindFolders(
    WellKnownFolderName.Inbox,
    new FolderView(int.MaxValue) { Traversal = FolderTraversal.Deep });
Jakob Christensen
  • 14,826
  • 2
  • 51
  • 81
4

You can page your requests and get the entire folder hierarchy from the server in just a few calls. The key is the FolderView.Traversal property, as Jacob indicates.

For example, for an Exchange mailbox with ~1,300 folders the code below only makes 2 requests. You can set your page size to whatever you like, as long as you stay at or below the server limit.

FYI: Exchange Online (Office365) caps at a maximum of 1,000 items in a response. I haven't tested, so I can't speak for any similar limits when querying an on-premises Exchange Server.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security;
using Exchange = Microsoft.Exchange.WebServices.Data; // from nuget package "Microsoft.Exchange.WebServices"

namespace FolderViewTraversal
{
    class Program
    {
        public static void Main()
        {
            Exchange.ExchangeService oService;
            Dictionary<string, User> oUsers;

            oUsers = new Dictionary<string, User>
            {
                { "User1", new User("write.to.me1@my.address.com", "Some-Fancy-Password1") },
                { "User2", new User("write.to.me2@my.address.com", "Some-Fancy-Password2") }
            };

            foreach (KeyValuePair<string, User> Credential in oUsers)
            {
                File.Delete(String.Format(LOG_FILE_PATH, Credential.Key)); 
            }
            foreach (KeyValuePair<string, User> Credential in oUsers)
            {
                LogFileName = Credential.Key;

                Console.WriteLine("Getting message counts for mailbox [{0}]...", LogFileName);
                Console.WriteLine();

                oService = Service.ConnectToService(Credential.Value);

                GetAllFolders(oService, String.Format(LOG_FILE_PATH, Credential.Key));

                Console.Clear();
            };

            Console.WriteLine();
            Console.Write("Press any key to exit...");
            Console.ReadKey();
        }

        private static void GetAllFolders(Exchange.ExchangeService Service, string LogFilePath)
        {
            Exchange.ExtendedPropertyDefinition oIsHidden = default;
            List<Exchange.Folder> oFolders = default;
            Exchange.FindFoldersResults oResults = default;
            bool lHasMore = false;
            Exchange.Folder oChild = default;
            Exchange.FolderView oView = default;

            short nPageSize = 0;
            short nOffSet = 0;

            List<string> oPaths = default;
            List<string> oPath = default;

            oIsHidden = new Exchange.ExtendedPropertyDefinition(0x10f4, Exchange.MapiPropertyType.Boolean);
            nPageSize = 1000;
            oFolders = new List<Exchange.Folder>();
            lHasMore = true;
            nOffSet = 0;

            while (lHasMore)
            {
                oView = new Exchange.FolderView(nPageSize, nOffSet, Exchange.OffsetBasePoint.Beginning)
                {
                    PropertySet = new Exchange.PropertySet(Exchange.BasePropertySet.IdOnly)
                };
                oView.PropertySet.Add(oIsHidden);
                oView.PropertySet.Add(Exchange.FolderSchema.ParentFolderId);
                oView.PropertySet.Add(Exchange.FolderSchema.DisplayName);
                oView.PropertySet.Add(Exchange.FolderSchema.FolderClass);
                oView.PropertySet.Add(Exchange.FolderSchema.TotalCount);
                oView.Traversal = Exchange.FolderTraversal.Deep;

                oResults = Service.FindFolders(Exchange.WellKnownFolderName.MsgFolderRoot, oView);
                oFolders.AddRange(oResults.Folders);

                lHasMore = oResults.MoreAvailable;

                if (lHasMore)
                {
                    nOffSet += nPageSize;
                }
            }

            oFolders.RemoveAll(Folder => (bool)Folder.ExtendedProperties[0].Value == true);
            oFolders.RemoveAll(Folder => Folder.FolderClass != "IPF.Note");

            oPaths = new List<string>();

            oFolders.ForEach(Folder =>
            {
                oChild = Folder;
                oPath = new List<string>();

                do
                {
                    oPath.Add(oChild.DisplayName);
                    oChild = oFolders.SingleOrDefault(Parent => Parent.Id.UniqueId == oChild.ParentFolderId.UniqueId);
                } while (oChild != null);

                oPath.Reverse();
                oPaths.Add(String.Format("{0}{1}{2}", String.Join(DELIMITER, oPath), '\t', Folder.TotalCount));
            });

            oPaths.RemoveAll(Path => Path.StartsWith("Sync Issues"));

            File.WriteAllText(LogFilePath, String.Join(Environment.NewLine, oPaths));
        }

        private static string LogFileName;
        private const string LOG_FILE_PATH = "D:\\Emails\\Remote{0}.txt";
        private const string DELIMITER = "\\";
    }

    internal class Service
    {
        public static Exchange.ExchangeService ConnectToService(User User)
        {
            return Service.ConnectToService(User, null);
        }

        public static Exchange.ExchangeService ConnectToService(User User, Exchange.ITraceListener Listener)
        {
            Exchange.ExchangeService oService = default;

            oService = new Exchange.ExchangeService(Exchange.ExchangeVersion.Exchange2013_SP1)
            {
                Credentials = new NetworkCredential(User.EmailAddress, User.Password)
            };
            oService.AutodiscoverUrl(User.EmailAddress, RedirectionUrlValidationCallback);

            if (Listener != null)
            {
                oService.TraceListener = Listener;
                oService.TraceEnabled = true;
                oService.TraceFlags = Exchange.TraceFlags.All;
            }

            return oService;
        }

        private static bool RedirectionUrlValidationCallback(string RedirectionUrl)
        {
            var _with1 = new Uri(RedirectionUrl);
            return _with1.Scheme.ToLower() == "https";
        }
    }

    internal class User
    {
        public string EmailAddress { get; }
        public SecureString Password { get; }

        public User(string EmailAddress)
        {
            this.EmailAddress = EmailAddress;
            this.Password = new SecureString();
        }

        public User(string EmailAddress, string Password)
        {
            this.EmailAddress = EmailAddress;
            this.Password = new SecureString();

            foreach(char Chr in Password) { this.Password.AppendChar(Chr); };

            this.Password.MakeReadOnly();
        }

        public static User GetUser()
        {
            Console.Write("Enter email address: ");
            string sEmailAddress = Console.ReadLine();
            Console.Write("Enter password: ");

            User functionReturnValue = new User(sEmailAddress);

            while (true)
            {
                ConsoleKeyInfo oUserInput = Console.ReadKey(true);
                if (oUserInput.Key == ConsoleKey.Enter)
                {
                    break; // TODO: might not be correct. Was : Exit While

                }
                else if (oUserInput.Key == ConsoleKey.Escape)
                {
                    functionReturnValue.Password.Clear();

                }
                else if (oUserInput.Key == ConsoleKey.Backspace)
                {
                    if (functionReturnValue.Password.Length != 0)
                    {
                        functionReturnValue.Password.RemoveAt(functionReturnValue.Password.Length - 1);
                    }

                }
                else
                {
                    functionReturnValue.Password.AppendChar(oUserInput.KeyChar);
                    Console.Write("*");

                }
            }

            if (functionReturnValue.Password.Length == 0)
            {
                functionReturnValue = null;
            }
            else
            {
                functionReturnValue.Password.MakeReadOnly();
                Console.WriteLine();
            }
            return functionReturnValue;
        }

    }

    internal class TraceListener : Exchange.ITraceListener
    {

        public void Trace(string TraceType, string TraceMessage)
        {
            File.AppendAllText(String.Format("{0}.txt", Path.Combine("D:\\Emails\\TraceOutput", Guid.NewGuid().ToString("D"))), TraceMessage);
        }
    }
}
Stephen Turner
  • 7,125
  • 4
  • 51
  • 68
InteXX
  • 6,135
  • 6
  • 43
  • 80
  • @garfbradaz: Appreciate you saying so. I disagree that it's worth a downvote—translation is dead simple these days—but that's another point altogether. So if I edit and translate to C#, will that fix things in your view? – InteXX Sep 13 '16 at 20:14
  • 2
    @garfbradaz — If I translate to C# and post an edit, will that earn back the vote? – InteXX Feb 27 '17 at 22:18
  • @garfbradaz — You took the trouble to explain *why* you assailed my reputation—appreciate that—but you've yet to answer my follow-up question. Again: if I translate to C# and post an edit, will that earn back the vote? – InteXX May 16 '17 at 18:27
  • of course I will :) I will even write a +1 on your comment – garfbradaz May 17 '17 at 06:04
  • @garfbradaz — I think that should do it. I didn't test it in C#, but since it works in VB I'm confident it works here. – InteXX May 17 '17 at 16:51
  • @garfbradaz — Got it! – InteXX May 20 '17 at 17:04
  • @garfbradaz — :-) – InteXX May 20 '17 at 23:13
  • It is not compiling - lot of errors. I am using VS2017 (V15.2), .NET FW 4.5.2 – Ahmad Agbaryah Mar 22 '18 at 08:42
  • @AhmadAgbaryah: Details, please. – InteXX Mar 23 '18 at 07:32
  • 1
    @AhmadAgbaryah: I've fixed up this code and got it compiling in a .Net Framework 4.8 Console. Though there is a lot of code in there that is not particularly relevant to the question. – Stephen Turner Jun 04 '21 at 21:51
  • Thanks. It was a conversion from one of my old VB.NET projects, and I should have checked it for build before posting it. Good job. | @StephenTurner – InteXX Jun 05 '21 at 04:55