cancel
Showing results for 
Search instead for 
Did you mean: 
Reply
Highlighted
Frequent Visitor

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

 

12 REPLIES 12
Highlighted
Helper IV
Helper IV

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

Highlighted

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

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?

Highlighted

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

Highlighted

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

Highlighted
New Member

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.  

Highlighted

Hi @hammer,

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

 

thanks a lot!

Highlighted

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)...

Highlighted
New Member

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");
}

 

 

Highlighted

Thanks @hammer you saved me a ton of time getting up and running with a bug tracking implementation using Power Automate Flow and an Azure Function similar to the one you shared. 

 

Also FYI to anyone who also ran into the issue with the DevOps "Send an HTTP request to Azure DevOps" not handling image uploads properly I think I may have found out why. See github @obvioussean's comment here:

https://github.com/MicrosoftDocs/vsts-rest-api-specs/issues/211#issuecomment-503719409

 

Apparently the DevOps Flow connector for "Send an HTTP request to Azure DevOps" is not sending the binary content as true binary but rather as a string. This explains why the image that gets "uploaded" is corrupted. 

 

If anyone else was referring to the DevOps API for attachments when implementing this you were probably also deceived by the request body portion showing a String type. According to github user @obvioussean in the same issue he mentions that the documentation system created this incorrectly.  

Highlighted

@naulacambra Do you know how to load json files to power automate to create a flow? 

Sounds like your solution you shared via json file is the flow I need. 

Highlighted

Hi @ka05th30ry ,

 

just have exactly your described problem with DevOps. 

Have an successfull flow with "Send an HTTP request to Azure DevOps" and cannot open my images. 

See also my issue: https://powerusers.microsoft.com/t5/Using-Flows/Send-an-HTTP-request-to-Azure-DevOps-Successfull-but...

 

So I guess you know the solution how to send the binary content correctly?

Just using the "HTTP" connector instead of the DevOps one? If so, how to create the binary content?

 

Have already tried it with the "HTTP" connector with following body:

 

base64ToBinary(items('Apply_to_each')?['contentBytes'])
 
...but same issue.
 
Would be great if you could help me.
 
Thank you!
 
Cheers,
Sven

Helpful resources

Announcements
Community Conference

Power Platform Community Conference

Check out the on demand sessions that are available now!

Power Platform ISV Studio

Power Platform ISV Studio

ISV Studio is designed to become the go-to Power Platform destination for ISV’s to monitor & manage published applications.

Top Solution Authors
Top Kudoed Authors
Users online (7,325)