cancel
Showing results for 
Search instead for 
Did you mean: 
Reply
naulacambra
Level: Powered On

Create work item at Azure DevOps from an email with attachments

My goal here is to crete work items whenever an email arrives.

 

I've successfully created a flow to create a Work Item at my Azure DevOps board when an email arrives.

 

The issue comes when I try to attach the email attachments to the created Work Item.

 

Since there isn't any "direct" way to do it with an action, I've tried something like this solution where it proposes use the Azure DevOps API service to attach the attachment.

 

According to Azure DevOps Api Documentation (here), to attach a file, I should send the binary data as a json, and I've been trying to achieve that with this flow

Capture.PNG

 Capture2.PNG

{
    "type": "object",
    "properties": {
        "Id": {
            "type": "string"
        },
        "Name": {
            "type": "string"
        },
        "ContentBytes": {
            "type": "string"
        },
        "ContentType": {
            "type": "string"
        },
        "Size": {
            "type": "integer"
        }
    }
}

 

 

Capture3.PNG

binary(body('Parse_JSON')?['ContentBytes'])

 

But it doesn't work. Anyone managed to successfully do this? Any hint would be helpful.

 

Thank you

 

9 REPLIES 9
tfhegdbn
Level: Powered On

Re: Create work item at Azure DevOps from an email with attachments

Hi ,

 

Maybe we can turn to @Pieter_Veenstra, and there was a section on "send an http request to sharepoint" in his blog. I think if he has time, he might be able to update the blog about "send an http request to Azure DevOps". That will help us a lot.Smiley Happy

Super User
Super User

Re: Create work item at Azure DevOps from an email with attachments

Hi @naulacambra,

 

I don't have the Azure Devops setup  to test this out, but lookling at the documentation link that was included in the previous post:

 

https://docs.microsoft.com/en-us/rest/api/vsts/wit/attachments/create?view=vsts-rest-4.1#upload_a_bi...

 

I would say that you need to format the body correctly So it should look somethign like the below JSON. As a test you probably could copy exactly that jason into the body and see if the HTTP request works. Once that works then replace the binary data in the JSON array with your data.

 

{
  "0": 137,
  "1": 80,
  "2": 78,
  "3": 71,
  "4": 13,
  "5": 10,
  "6": 26,
  "7": 10,
  "8": 0,
  "9": 0,
  "10": 0,
  "11": 13,
  "12": 73,
  "13": 72,
  "14": 68,
  "15": 82,
  "16": 0,
  "17": 0,
  "18": 0,
  "19": 24,
  "20": 0,
  "21": 0,
  "22": 0,
  "23": 24,
  "24": 8,
  "25": 2,
  "26": 0,
  "27": 0,
  "28": 0,
  "29": 111,
  "30": 21,
  "31": 170,
  "32": 175,
  "33": 0,
  "34": 0,
  "35": 0,
  "36": 1,
  "37": 115,
  "38": 82,
  "39": 71,
  "40": 66,
  "41": 0,
  "42": 174,
  "43": 206,
  "44": 28,
  "45": 233,
  "46": 0,
  "47": 0,
  "48": 0,
  "49": 4,
  "50": 103,
  "51": 65,
  "52": 77,
  "53": 65,
  "54": 0,
  "55": 0,
  "56": 177,
  "57": 143,
  "58": 11,
  "59": 252,
  "60": 97,
  "61": 5,
  "62": 0,
  "63": 0,
  "64": 0,
  "65": 9,
  "66": 112,
  "67": 72,
  "68": 89,
  "69": 115,
  "70": 0,
  "71": 0,
  "72": 14,
  "73": 195,
  "74": 0,
  "75": 0,
  "76": 14,
  "77": 195,
  "78": 1,
  "79": 199,
  "80": 111,
  "81": 168,
  "82": 100,
  "83": 0,
  "84": 0,
  "85": 0,
  "86": 101,
  "87": 73,
  "88": 68,
  "89": 65,
  "90": 84,
  "91": 56,
  "92": 79,
  "93": 237,
  "94": 204,
  "95": 65,
  "96": 10,
  "97": 192,
  "98": 32,
  "99": 12,
  "100": 68,
  "101": 81,
  "102": 239,
  "103": 127,
  "104": 105,
  "105": 27,
  "106": 240,
  "107": 167,
  "108": 24,
  "109": 146,
  "110": 52,
  "111": 22,
  "112": 138,
  "113": 80,
  "114": 240,
  "115": 237,
  "116": 156,
  "117": 140,
  "118": 211,
  "119": 250,
  "120": 71,
  "121": 206,
  "122": 80,
  "123": 109,
  "124": 227,
  "125": 80,
  "126": 83,
  "127": 188,
  "128": 19,
  "129": 213,
  "130": 217,
  "131": 34,
  "132": 141,
  "133": 164,
  "134": 55,
  "135": 190,
  "136": 70,
  "137": 104,
  "138": 88,
  "139": 73,
  "140": 90,
  "141": 161,
  "142": 55,
  "143": 137,
  "144": 162,
  "145": 53,
  "146": 180,
  "147": 213,
  "148": 198,
  "149": 33,
  "150": 210,
  "151": 60,
  "152": 31,
  "153": 130,
  "154": 33,
  "155": 65,
  "156": 87,
  "157": 249,
  "158": 68,
  "159": 140,
  "160": 230,
  "161": 109,
  "162": 105,
  "163": 200,
  "164": 163,
  "165": 55,
  "166": 249,
  "167": 203,
  "168": 144,
  "169": 224,
  "170": 71,
  "171": 132,
  "172": 134,
  "173": 149,
  "174": 14,
  "175": 9,
  "176": 254,
  "177": 89,
  "178": 220,
  "179": 156,
  "180": 167,
  "181": 161,
  "182": 87,
  "183": 206,
  "184": 80,
  "185": 165,
  "186": 247,
  "187": 11,
  "188": 116,
  "189": 99,
  "190": 71,
  "191": 0,
  "192": 204,
  "193": 122,
  "194": 63,
  "195": 206,
  "196": 0,
  "197": 0,
  "198": 0,
  "199": 0,
  "200": 73,
  "201": 69,
  "202": 78,
  "203": 68,
  "204": 174,
  "205": 66,
  "206": 96,
  "207": 130,
  "208": 0,
  "209": 0,
  "BYTES_PER_ELEMENT": 1,
  "buffer": {
    "0": 137,
    "1": 80,
    "2": 78,
    "3": 71,
    "4": 13,
    "5": 10,
    "6": 26,
    "7": 10,
    "8": 0,
    "9": 0,
    "10": 0,
    "11": 13,
    "12": 73,
    "13": 72,
    "14": 68,
    "15": 82,
    "16": 0,
    "17": 0,
    "18": 0,
    "19": 24,
    "20": 0,
    "21": 0,
    "22": 0,
    "23": 24,
    "24": 8,
    "25": 2,
    "26": 0,
    "27": 0,
    "28": 0,
    "29": 111,
    "30": 21,
    "31": 170,
    "32": 175,
    "33": 0,
    "34": 0,
    "35": 0,
    "36": 1,
    "37": 115,
    "38": 82,
    "39": 71,
    "40": 66,
    "41": 0,
    "42": 174,
    "43": 206,
    "44": 28,
    "45": 233,
    "46": 0,
    "47": 0,
    "48": 0,
    "49": 4,
    "50": 103,
    "51": 65,
    "52": 77,
    "53": 65,
    "54": 0,
    "55": 0,
    "56": 177,
    "57": 143,
    "58": 11,
    "59": 252,
    "60": 97,
    "61": 5,
    "62": 0,
    "63": 0,
    "64": 0,
    "65": 9,
    "66": 112,
    "67": 72,
    "68": 89,
    "69": 115,
    "70": 0,
    "71": 0,
    "72": 14,
    "73": 195,
    "74": 0,
    "75": 0,
    "76": 14,
    "77": 195,
    "78": 1,
    "79": 199,
    "80": 111,
    "81": 168,
    "82": 100,
    "83": 0,
    "84": 0,
    "85": 0,
    "86": 101,
    "87": 73,
    "88": 68,
    "89": 65,
    "90": 84,
    "91": 56,
    "92": 79,
    "93": 237,
    "94": 204,
    "95": 65,
    "96": 10,
    "97": 192,
    "98": 32,
    "99": 12,
    "100": 68,
    "101": 81,
    "102": 239,
    "103": 127,
    "104": 105,
    "105": 27,
    "106": 240,
    "107": 167,
    "108": 24,
    "109": 146,
    "110": 52,
    "111": 22,
    "112": 138,
    "113": 80,
    "114": 240,
    "115": 237,
    "116": 156,
    "117": 140,
    "118": 211,
    "119": 250,
    "120": 71,
    "121": 206,
    "122": 80,
    "123": 109,
    "124": 227,
    "125": 80,
    "126": 83,
    "127": 188,
    "128": 19,
    "129": 213,
    "130": 217,
    "131": 34,
    "132": 141,
    "133": 164,
    "134": 55,
    "135": 190,
    "136": 70,
    "137": 104,
    "138": 88,
    "139": 73,
    "140": 90,
    "141": 161,
    "142": 55,
    "143": 137,
    "144": 162,
    "145": 53,
    "146": 180,
    "147": 213,
    "148": 198,
    "149": 33,
    "150": 210,
    "151": 60,
    "152": 31,
    "153": 130,
    "154": 33,
    "155": 65,
    "156": 87,
    "157": 249,
    "158": 68,
    "159": 140,
    "160": 230,
    "161": 109,
    "162": 105,
    "163": 200,
    "164": 163,
    "165": 55,
    "166": 249,
    "167": 203,
    "168": 144,
    "169": 224,
    "170": 71,
    "171": 132,
    "172": 134,
    "173": 149,
    "174": 14,
    "175": 9,
    "176": 254,
    "177": 89,
    "178": 220,
    "179": 156,
    "180": 167,
    "181": 161,
    "182": 87,
    "183": 206,
    "184": 80,
    "185": 165,
    "186": 247,
    "187": 11,
    "188": 116,
    "189": 99,
    "190": 71,
    "191": 0,
    "192": 204,
    "193": 122,
    "194": 63,
    "195": 206,
    "196": 0,
    "197": 0,
    "198": 0,
    "199": 0,
    "200": 73,
    "201": 69,
    "202": 78,
    "203": 68,
    "204": 174,
    "205": 66,
    "206": 96,
    "207": 130,
    "208": 0,
    "209": 0,
    "byteLength": 210
  },
  "length": 210,
  "byteOffset": 0,
  "byteLength": 210
}

Highlighted
naulacambra
Level: Powered On

Re: Create work item at Azure DevOps from an email with attachments

After some more tests, I've sucessfully sent the example json to AzureDevOps as attachments, but now, I have to transform the base64 image that I have at the email object into an array of bytes, such as the example from the documentation. Any tip?

maricel0422
Level: Powered On

Re: Create work item at Azure DevOps from an email with attachments

Hi @naulacambra,  I am trying to do the same... do you mind sharing the image of the flow you used to process the attachments?

naulacambra
Level: Powered On

Re: Create work item at Azure DevOps from an email with attachments

Hi @maricel0422 at the end I wasn't able to do it "properly". Even though, let me explain my workaround as it could work for you.

 

The mails had two types of attachments, the "in-body" attachments (images inside the email body) and the "attached" attachments. Both of them were described in an array ("attachments") at the end of email object.

 

First, about the first type of attachments;

When an email is received and the flow is triggered, the object received replaced the in-body "src" attribute of the images by something like "src=cid:XXXXXXXXXX@YYYYYYYY". I'm not sure if this is something exclusive of my email client (Outlook.com) or happens all the time. My solution here was to loop through the attachments array, looking for the "id" attribute in the body, and replace the "src" attribute by the base64 representation of the Content-Bytes.

 

About the second type;

My solution was to upload the attachment to OneDrive and then, append the share link into the body content (which later will be the Work Item description). Is not fancy, but it works.

 

Here you will find a link to the json template of my flow. It has a lot more functionalities, but I'm sure you'll be able to extract the part that you're interested in.

 

I hope this helps you

hammer
Level: Power Up

Re: Create work item at Azure DevOps from an email with attachments

I was able to do this using an Azure logic app and an Azure function, instead of flow.  Not sure of your familiarity but from a design/implementation perspective the two (logic app, flow) are similar.  I think my solution would work in flow, I just didn't add a custom connector which I think you need to do in order to call an Azure function from Flow.

 

So, using azure logic app:

 

  • Trigger:  When a new email arrives
    • Check for subject filter to exist
  • Action:  HTML to text (strip out html from email)
  • Action: Create a work item (DevOps)
  • Action:  HttpTrigger (Azure Function)
    • Pass in attachments from email
    • Azure function parses the body (attachments), and creates/attaches for each attachment (inline supported also) - basically just does 2 web requests (for each attachment passed in):
      • POST to create the attachment
      • PATCH to add the attachment to the specific work item
  • Action: Send user email with DevOps ticket # (id)

 

If anyone would like me to elaborate further or has any questions please let me know.  Tested and works.  

Middiu
Level: Power Up

Re: Create work item at Azure DevOps from an email with attachments

Hi @hammer,

would you mind to elaborate the Azure Function part? Even better if you could share your code directly.

 

thanks a lot!

CaptainKirk
Level: Power Up

dezRe: Create work item at Azure DevOps from an email with attachments

Yes, a more specific example of what you did (codez plz, LOL).

I am new to TFS and DevOps and we are replacing existing Mantis functionality here.

Attachements are a big part (we get screenshots of issues)...

hammer
Level: Power Up

Re: Create work item at Azure DevOps from an email with attachments

So, here is what I did on a more granular level, this also now uses MS Flow and not a logic application:

 

  1. Create Azure Function to handle uploading/associating an attachment to a DevOps workitem
    1. This will also need a DevOps API token
  2. Create an Azure API that will let you access the Azure Function via web request
  3. Send Attachment data in MS Flow using http request action to the Azure API

 

Microsoft Flow:

  • URI = URL created in Azure API that will hit the azure function
  • SubscriptionKey = Key for subscription in Azure API
  • Body:
    • Attachments - this comes from email trigger (not in pic)
    • Subject - this comes from email trigger
    • WorkItemId - this comes from 'create work item 2' step so the function knows which work item to work with

 

Azure Function Code:

#r "Newtonsoft.Json"

using System.Net;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;

public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    string name = req.Query["name"];
    string workItemId = req.Query["workItemId"];

    string tokenFromDevops = @"{TOKEN}";
    string urlDevopsOrgAddAttach = @"https://dev.azure.com/{ORGNAME}/_apis/wit/attachments?api-version=5.0&fileName=";

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    
    name = name ?? data?.name;
    workItemId = workItemId ?? data?.workItemId;

    foreach (var a in data.attachments)
    {
        string attachmentBytes = a.ContentBytes;
        string attachmentFileName = a.Name;
        var bytesFromB = Convert.FromBase64String(attachmentBytes);
        string url = urlDevopsOrgAddAttach + attachmentFileName;
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        request.KeepAlive = false;
        request.Method = "POST";
        request.Headers.Add("Authorization", "Basic " + tokenFromDevops);
        request.ContentType = "application/octet-stream";
        Stream requestStream = request.GetRequestStream();
        requestStream.Write(bytesFromB, 0, bytesFromB.Length);
        requestStream.Close();

        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        var rspUploadAttachment = new StreamReader(response.GetResponseStream()).ReadToEnd();
        dynamic jsonResponse = JsonConvert.DeserializeObject(rspUploadAttachment);
        String urlUploadedAttachment = jsonResponse.url;

        // now we need to associate the uploaded attachment to an actual work item
        string urlWorkItem = @"https://dev.azure.com/{ORGNAME}/{PROJECTNAME}/_apis/wit/workitems/" + workItemId + "?api-version=5.0";  
        
        HttpWebRequest requestAttach = (HttpWebRequest)WebRequest.Create(urlWorkItem);
        requestAttach.Method = "PATCH";
        requestAttach.Accept = "application/json";
        requestAttach.Headers.Add("Authorization", "Basic " + tokenFromDevops);
        requestAttach.ContentType = "application/json-patch+json";
        
        string jsonPatchAttach = @"[
  {
    'op': 'add',
    'path': '/fields/System.History',
    'value': 'Adding files from Azure automation'
  },
  {
    'op': 'add',
    'path': '/relations/-',
    'value': {
      'rel': 'AttachedFile',
      'url': 'urlReplaceToken',
      'attributes': {
        'comment': 'Attachment added from Azure automation'
      }
    }
  }
]";
        // this can just be done in json above, but left as-is
        jsonPatchAttach = jsonPatchAttach.Replace("urlReplaceToken", urlUploadedAttachment);
        using (var streamWriter = new StreamWriter(requestAttach.GetRequestStream()))
            {                
                streamWriter.Write(jsonPatchAttach);
                streamWriter.Flush();
           }

        HttpWebResponse responseAttach = (HttpWebResponse)requestAttach.GetResponse();
        var rspAddAttachment = new StreamReader(responseAttach.GetResponseStream()).ReadToEnd();
        // do whatever you want here with response - or nothing
        // dynamic jsonResponse = JsonConvert.DeserializeObject(rspAddAttachment);
    }

    return name != null
        ? (ActionResult)new OkObjectResult($"WorkItemID Created!")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

 

 

Helpful resources

Announcements
thirdimage

Power Automate Community User Group Member Badge

Fill out a quick form to claim your user group badge now!

firstImage

Incoming: New and improved badges!

We've given our badges an overhaul and also added some brand new ones!

fifthimage

Microsoft Learn

Learn how to build the business apps that you need.

sixthImage

Power Platform World Tour

Find out where you can attend!

seventhimage

Webinars & Video Gallery

Watch & learn from the Power Automate Community Video Gallery!

Top Kudoed Authors
Users Online
Currently online: 100 members 5,372 guests
Please welcome our newest community members: