Sync Visual Studio Online items to Outlook task list

As a consultant, working in a team of 6 people, task management is a crucial part in my daily business. In my team, it is very important to have a good task management, in order to decide about who does the workshop for a customer next week, or who can do the conception phase during the next few weeks. For this reason we decided to use Visual Studio Online (VSO) in order to manage our work items and keep an eye on the team members’ workloads, although we’re no developers or engineers, who need to manage code. But the point is that there is one major part missing, when managing a team’s tasks in VSO - the Outlook integration. Because personal tasks are something, which are managed in Outlook, to organise the daily work based on the calendar and the open tasks. But the missing point is that there is no native integration of VSO tasks and work items with Outlook. So many people I know overcome this issue by creating a task in Outlook everytime they get a new work item assigned in VSO. But this is too time consuming in my opinion.

Therefore, I decided to search for an alternative and tried some the 3rd party addins for Outlook, which are designed to intergrate VSO in the Outlook client. But I have not found an addin, which is performant and which also makes my VSO tasks available on my mobile device. And with the releas to Microsoft’s To-Do, I decided to do it on my own and create my VSO to Outlook task sync engine. And this was quite easy (and as I’m not a developer you can do it too, im sure). My goal was that I didn’t want to setup any local resources or servers, but to keep it as simple as possible. So I decided on the following design:

/static/img/azure_function_design.png

So how to achieve this? First of all, we need to create an Azure Function app, with the following properties:

/static/img/azure_function01-300x494.png

After the function is created, navigate to the function app page and create a new function and get the function URL:

/static/img/azure_function02-830x99.png

Then we’re ready to insert the following code, which basically does all our work:

#r "Newtonsoft.Json"
#r "Microsoft.Exchange.WebServices.dll"
using System;
using System.Net;
using Newtonsoft.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using Microsoft.Exchange.WebServices.Data;

public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
{
    log.Info($"Webhook was triggered!");

    string jsonContent = await req.Content.ReadAsStringAsync();
    dynamic data = JsonConvert.DeserializeObject(jsonContent);
    HttpContent requestContent = req.Content;
    string resource = jsonContent.Substring(jsonContent.IndexOf("resource"));
    log.Info("Item info: " + resource);

    var assignedTo = (object)data["resource"]["fields"]["System.AssignedTo"]["newValue"];
    var title = (object)data["resource"]["revision"]["fields"]["System.Title"];
    var description = (object)data["resource"]["revision"]["fields"]["System.Description"];
    var iteration = (object)data["resource"]["revision"]["fields"]["System.IterationPath"];
    var id = (object)data["resource"]["workItemId"];
    string assigned = JsonConvert.SerializeObject(assignedTo);
    string bliTitle = JsonConvert.SerializeObject(title);
    string bliDescription = JsonConvert.SerializeObject(description);
    string bliIteration = JsonConvert.SerializeObject(iteration);
    string itemID = JsonConvert.SerializeObject(id);
    string itemUrl = "https://solvion.visualstudio.com/DefaultCollection/ICS/_workitems/edit/"+itemID.ToString();
    bliIteration = bliIteration.Replace("\"", "");
    bliTitle = bliTitle.Replace("\"", "");
    bliDescription = bliDescription.Replace("\"", "");
    string assignedNew = assigned.ToString();
    string ite = bliIteration.ToString();

  //Replace with your user ID
    string helper = "<UserID of VSO>";
    assignedNew = assignedNew.Replace("\"", "");

    if (assignedNew == helper) {
        log.Info("Start creating task");
        ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2013_SP1);
    //Replace with your Exchange (Online) admin credentials
        service.Credentials = new WebCredentials("<Exchange admin username>", "<Exchange admin password>");
        //Replace with your mail address
        service.AutodiscoverUrl("<Mail address>", RedirectionUrlValidationCallback);

        //Check if iteration is blank or if the item is already assigned to an iteration
    //Replace with your project's name or area path in VSO
        if (ite == "<VSO project name>") {
            DateTime genericStartDate = DateTime.Now;
            DateTime genericEndDate = DateTime.Now.AddDays(7);
            string finalStartDate = genericStartDate.ToString("dd/MM/yyyy");
            string finalDueDate = genericEndDate.ToString("dd/MM/yyyy");
            Dictionary<string, string> dictionary = new Dictionary<string, string>();
            bliDescription = itemUrl + " \n " + bliDescription;
            dictionary.Add("Subject", bliTitle);
            dictionary.Add("StartDate", finalStartDate);
            dictionary.Add("DueDate", finalDueDate);
            dictionary.Add("Body", bliDescription);

      //Replace with your mail address
            CreateTaskItem("<Mail address>", service, dictionary);
            return req.CreateResponse(HttpStatusCode.OK);

        } else {
            string[] dates = ite.Split(' ');
            string datenew = dates[2];
            string startDate = datenew.Replace("\"", "");
            startDate = startDate.Replace("(", "");
            startDate = startDate.Replace(")", "");
            string[] startEndDates = startDate.Split('-');
            string startDateNew = startEndDates[0] + "2017";
            string endDateNew = startEndDates[1] + "2017";
            string finalStartDate = startDateNew.Replace(".", "/");
            string finalDueDate = endDateNew.Replace(".", "/");
            bliDescription = itemUrl + " \n " + bliDescription;
            Dictionary<string, string> dictionary = new Dictionary<string, string>();
            dictionary.Add("Subject", bliTitle);
            dictionary.Add("StartDate", finalStartDate);
            dictionary.Add("DueDate", finalDueDate);
            dictionary.Add("Body", bliDescription);

      //Replace with your mail address
            CreateTaskItem("<Mail address>", service, dictionary);
            return req.CreateResponse(HttpStatusCode.OK);
        }
    } else {
        return req.CreateResponse(HttpStatusCode.OK);
    }

    if (data == null) {
        return req.CreateResponse(HttpStatusCode.BadRequest, new {
            error = "Please pass first/last properties in the input object"
        });
    }

}

private static bool RedirectionUrlValidationCallback(string redirectionUrl)
{
   // The default for the validation callback is to reject the URL.
   bool result = false;

   Uri redirectionUri = new Uri(redirectionUrl);

   // Validate the contents of the redirection URL. In this simple validation
   // callback, the redirection URL is considered valid if it is using HTTPS
   // to encrypt the authentication credentials. 
   if (redirectionUri.Scheme == "https")
   {
      result = true;
   }
   return result;
}

public static void CreateTaskItem(string targetMailId, ExchangeService EXservice, Dictionary<string, string> dictionary)
{
    Microsoft.Exchange.WebServices.Data.Task task = new Microsoft.Exchange.WebServices.Data.Task(EXservice);

    DateTime convertedStartDate = DateTime.ParseExact(dictionary["StartDate"], "dd/MM/yyyy", System.Globalization.CultureInfo.CurrentUICulture.DateTimeFormat);
    DateTime convertedDueDate = DateTime.ParseExact(dictionary["DueDate"], "dd/MM/yyyy", System.Globalization.CultureInfo.CurrentUICulture.DateTimeFormat);
    task.Subject = dictionary["Subject"];
    task.DueDate = convertedDueDate;
    task.StartDate = convertedStartDate;
    task.Body = dictionary["Body"]; 
    task.Save(new FolderId(WellKnownFolderName.Tasks,targetMailId));
}

In the code I’m referencing the Exchange WebServices API. In order to use that API in your code, you have to download the API from https://www.microsoft.com/en-us/download/details.aspx?id=42022 and install it on your machine. After installing it, you can find the file in your %ProgramFiles%\Microsoft\Exchange\ directory. Now you need to head over to the function’s debug console:

/static/img/azure_function08-768x596.png

/static/img/azure_function09-480x324.png

Now navigate to the path D:\home\site\wwwroot<nameofyourfunction> and create a bin folder:

/static/img/azure_function10-480x713.png

Now navigate to that bin folder and drag and drop your Microsoft.Exchange.WebServices.dll file in there.

After the function is in place, we need to head over to VSO and create a service hook as follows:

/static/img/azure_function03-480x445.png

On the first page select “Work item updated” and your area path. On the next page insert your function’s URL and hit “Test” or “Finish”, as this is it on the VSO side.

/static/img/azure_function04-480x520.png

Now you can create a new work item (either in a sprint or in the backlog) and assign it to your user.

/static/img/azure_function05-768x395.png

If you have configured everything correct, you should see that the Azure function is triggered in the logs:

/static/img/azure_function06-768x172.png

And after a couple of seconds you should see the task in your Outlook task list (unfortunately my Office is in German - sorry guys):

/static/img/azure_function07-768x288.png

And whats even cooler now is this:

/static/img/20170429_215302000_iOS.png

The task is also showing up on my mobile device in the To-Do app :) and I can keep track of my tasks, when I’m traveling too.

But this is basically it. As I already said, I’m not an engineer, so the code could be improved im pretty sure, but it does what I want it to do, so I’m fine for the moment…

But if you have some improvements please let me know!

Maybe I’ll follow up with a second part on assigning the task to a category in Outlook as well, we’ll see…