Add voting buttons to the message with the Microsoft Graph SDK for .NET

21/06/2023
C#Graph APIMAPI

As mentioned in previous articles, the Graph API doesn't expose all properties for resources like messages, events, contacts, and mailFolders. When a property is defined on the server, it can be accessed from the Graph API through extended properties.

One of the properties of the message resource type that is not exposed by the Graph API are voting buttons. MAPI has the property PidLidVerbStream which specifies what voting responses the user can make in response to the message.

MAPI data type PT_BINARY is equivalent to the Binary type of the single-value extended property in the Graph API. The property long ID is 0x00008520 and the guid of the namespace is {00062008-0000-0000-C000-000000000046}. The id of the single-value extended property will be Binary {00062008-0000-0000-C000-000000000046} Id 0x00008520.

The Graph API returns values of binary type as base-64 encoded strings. The app that reads the binary type must encode or decode the value by itself.

To be able to decode or encode the stream, we need to know the format of the VerbStream structure.

VerbStream structure

See the format of the VerbStream property:

Property Number of bytes Description
Version 2 Set to 0x0102
Count 4 Specifies the count of VoteOptions and VoteOptionExtras
VoteOption1 variable length 1st VoteOption
VoteOption2 variable length 2nd VoteOption
...
VoteOptionN variable length Nth VoteOption
Version2 2 Set to 0x0104
VoteOptionExtras1 variable length 1st VoteOptionExtras
VoteOptionExtras2 variable length 2nd VoteOptionExtras
...
VoteOptionExtrasN variable length Nth VoteOptionExtras

The verb stream starts with the Version and Count fields, followed by two parallel arrays of VoteOption and VoteOptionExtra structures separated by the Version2 field.

Each element in those two arrays, when combined, describes a single voting option that can be taken by the user in response to the message.

VoteOption structure

The VoteOption has the following format:

Property Number of bytes Description
VerbType 4 The used verb
DisplayNameCount 1 The count of characters in the DisplayName field
DisplayName variable The localized display name of the voting option as an ANSI string. Max length is 255 characters
MsgClsNameCount 1 The count of characters in the MsgClassName field. Set to 8 (0x08)
MsgClsName variable Set to "IPM.Note"
Internal1StringCount 1 The count of characters in the Internal1String field
Internal1String variable Not present if Internal1StringCount is 0x00
DisplayNameCountRepeat 1 Has the same value as the DisplayNameCount field
DisplayNameRepeat variable Has the same value as the DisplayName field. Max length is 255 characters
Internal2 4 Set to 0x00000000
Internal3 1 Set to 0x00
fUseUSHeaders 4 Indicates that a U.S.style reply header is to be used in the response
Internal4 4 Set to 0x00000001
SendBehavior 4 Specifies whether the user is to be prompted to edit the response mail or whether the client automatically sends it on behalf of the user
Internal5 4 Set to 0x00000002
ID 4 Specified a numeric identifier of voting option. The value must monotonically increase for each subsequent vote option
Internal6 4 Set to 0xFFFFFFFF

VoteOptionExtras structure

The VoteOptionExtras has the following structure:

Property Number of bytes Description
DisplayNameCount 1 The count of Unicode characters in the DisplayName field
DisplayName variable The display name of the voting option, as a Unicode string. Max length is 255 characters
DisplayNameCountRepeat 1 The count of Unicode characters in the DisplayNameRepeat field. Must have the same value as the DisplayNameCount field
DisplayNameRepeat variable The same value as it is in the DisplayName field

Voting response

The PidLidVerbResponse canonical property specifies the voting option that a respondent selected. MAPI's data type is PT_UNICODE, which is equivalent to the String property in the Graph API. The long ID 0x00008524 is and the GUID of the namespace is {00062008-0000-0000-C000-000000000046}.

The id of the single-value extended property will be String {00062008-0000-0000-C000-000000000046} Id 0x00008524.

Voting flow

The whole process of voting has several steps:

  1. The sender sends a voting message to the recipients (voters). The message contains a well-formed PidLidVerbStream property; the rest of the message is identical to a nonvoting message.

  2. The voters receive the message. The client mail app must check for the existence of the PidLidVerbStream property and display voting options to the user.

  3. When a voter selects a voting option, a specifically crafted response mail is generated and sent back to the sender. The message must contain the PidLidVerbResponse property with the voting option that the voter selected.

  4. The sender's client app must process the received response messages and aggregate them for display to the user who initiated the voting.

At each step in the process, the voting messages that are sent are identical to the nonvoting messages.

The only difference is the presence of either the PidLidVerbStream property or the PidLidVerbResponse property.

Encode and decode verb stream

I've created the helper class PidLidVerbStreamConverter which converts binary data encoded as base-64 to an object that represents the verb stream. It also converts a list of voting options to base-64 string.

Internally, the PidLidVerbStreamConverter class uses BitConverter class that can convert bytes or byte span to a specified base type and backward.

Methods for decoding:

Destination type BitConverter method
byte Can be read directly without conversion
ushort ushort ToUInt16(ReadOnlySpan<byte> value)
uint uint ToUInt32(ReadOnlySpan<byte> value)

Methods for encoding:

Source type BitConvert method
byte Can be written directly
ushort bool TryWriteBytes(Span<byte> destination, ushort value)
uint bool TryWriteBytes(Span<byte> destination, uint value)

The Microsoft Graph .NET Client Library

Let's check how to encode/decode the value of PidLidVerbStream in C#.

To integrate the Microsoft Graph API into .NET project, search for and install Microsoft.Graph NuGet in the NuGet Library, or install the Microsoft Graph .NET Client Library through the Package Manager Console with the command:

Install-Package Microsoft.Graph

To be able to call the Graph API, we need to initialize GraphServiceClient. The GraphServiceClient constructor accepts instances of TokenCredential from Azure.Identity (NuGet has the same name) or an instance of the class that implements either IRequestAdapter or IAuthenticationProvider interface.

var graphClient = new GraphServiceClient(requestAdapter);

Create voting message

The voting message is identical to the nonvoting message, the only difference being that it contains the extended property.

var voteOptions = new List<string> { "Office", "Meeting room", "Lab", "Relax room" };
var verbStreamBase64 = PidLidVerbStreamConverter.ToBase64String(voteOptions);

var body = new SendMailPostRequestBody
{
    Message = new Message
    {
        ToRecipients = new List<Recipient>
        {
            new Recipient
            {
                EmailAddress = new EmailAddress
                {
                    Address = "john.doe@contoso.com"
                }
            },
            new Recipient
            {
                EmailAddress = new EmailAddress
                {
                    Address = "adele.vence@contoso.com"
                }
            }
        },
        Subject = "Please vote",
        Body = new ItemBody
        {
            ContentType = BodyType.Text,
            Content = "Where would you like to meet?"
        },
        SingleValueExtendedProperties = new List<SingleValueLegacyExtendedProperty>
        {
            new SingleValueLegacyExtendedProperty
            {
                Id = "Binary {00062008-0000-0000-C000-000000000046} Id 0x8520",
                Value = verbStreamBase64
            }
        }
    }
};
await client.Me.SendMail.PostAsync(body);

The recipients will receive the message which includes voting buttons.

Read voting message

Filter messages with voting buttons:

var selectedProperties = new string[] { "id", "singleValueExtendedProperties", "subject" };

var lid = "0x8520";
var pidLidVerbStreamId = $"Binary {{00062008-0000-0000-C000-000000000046}} Id {lid}";
var filterQuery = $"singleValueExtendedProperties/any(r:r/id eq '{pidLidVerbStreamId}' and cast(r/value,Edm.Binary) ne null)";
var expandQuery = new string[] { $"singleValueExtendedProperties($filter=id eq '{pidLidVerbStreamId}')" };
var response = await graphClient.Me.Messages.GetAsync((requestConfiguration) =>
{
    requestConfiguration.QueryParameters.Select = selectedProperties;
    requestConfiguration.QueryParameters.Filter = filterQuery;
    requestConfiguration.QueryParameters.Expand = expandQuery;
});

var messages = response.Value;

When filtering messages that have the extended property Binary {00062008-0000-0000-C000-000000000046} Id 0x8520, the filter for the value of the extended property must be specified; otherwise the request will fail.

The value is binary data, so we need to cast the value to Edm.Binary and check if binary data is not null.

The filter query doesn't ensure that the singleValueExtendedProperties will be included in the response, so we need to declare it in the expand query.

Now, read the SingleValueExtendedProperties of each message:

foreach (var message in messages)
{
    var pidLidVerbStream = message.SingleValueExtendedProperties?.FirstOrDefault(x => x.Id.Contains(lid));
    if (pidLidVerbStream != null)
    {
        var verbStream = PidLidVerbStreamConverter.GetFromBase64(pidLidVerbStream.Value);
    }
}

Note: The Graph API returns Binary {00062008-0000-0000-c000-000000000046} Id 0x8520 for the extended property but the id in filter and expand query can be specified in different formats.

The Graph API will handle different formats of the extended property id. All of the formats below are equivalent:

Binary {00062008-0000-0000-c000-000000000046} Id 0x8520
Binary {00062008-0000-0000-c000-000000000046} Id 0x00008520
Binary {00062008-0000-0000-c000-000000000046} Id 34080
Binary {00062008-0000-0000-C000-000000000046} Id 0x8520
Binary {00062008-0000-0000-C000-000000000046} Id 0x00008520
Binary {00062008-0000-0000-C000-000000000046} Id 34080

Voting button options (VoteOption structure) have the value of the VerbType property equals to 4.

var votingOptions = verbStream.VoteOptions.Where(x => x.VerbType == 4)

Send voting response

When voters send the voting response, they simply reply to the sender of the voting options. The voting response is specified by the PidLidVerbResponse property. The standard requires a subject prefix with the response as well.

// selected voting option - one of the values of the DisplayName field in the VoteOption structure
var votingOption = "Lab";
var body = new ReplyPostRequestBody
{
    Message = new Message
    {
        Subject = $"{votingOption}: Please vote",
        Body = new ItemBody
        {
            ContentType = BodyType.Text,
            Content = string.Empty
        },
        SingleValueExtendedProperties = new List<SingleValueLegacyExtendedProperty>
        {
            new SingleValueLegacyExtendedProperty
            {
                Id = "String {00062008-0000-0000-C000-000000000046} Id 0x8524",
                Value = votingOption
            },
            new SingleValueLegacyExtendedProperty
            {
                Id = "Integer {00062008-0000-0000-C000-000000000046} Id 0x851A",
                Value = "1"
            }
        }
    }
};
// message id 
await client.Me.Messages[messageId].Reply.PostAsync(body);

It's good practice to also set the PidLidAutoProcessState property. It specifies the options used in the processing of voting and tracking for e-mail messages.

0x00000000 - The client will not process the voting and tracking for the message.

0x00000001 - The client will process the voting and tracking when the message is received or opened.

0x00000002 - The client will process the voting and tracking only when the message is opened.

Read response for voting

The MAPI property PidLidVerbResponse specifies the voting option that a respondent has selected. Corresponds to one of the values of the DisplayName field in the VoteOption structure.

var selectedProperties = new string[] { "id", "singleValueExtendedProperties", "subject" };

var pidLidVerbResponseId = "String {00062008-0000-0000-C000-000000000046} Id 0x8524";
var filterQuery = $"singleValueExtendedProperties/any(r:r/id eq '{pidLidVerbResponseId}' and r/value ne null)";
var expandQuery = new string[] { $"singleValueExtendedProperties($filter=id eq '{pidLidVerbResponseId}')" };
var response = await client.Me.Messages.GetAsync((requestConfiguration) =>
{
    requestConfiguration.QueryParameters.Select = selectedProperties;
    requestConfiguration.QueryParameters.Filter = filterQuery;
    requestConfiguration.QueryParameters.Expand = expandQuery;
});

var messages = response.Value;

foreach (var message in messages)
{
    var pidLidVerbResponse = message.SingleValueExtendedProperties?.FirstOrDefault(x => x.Id.Contains("0x8524"));
    var selectedVoteOption = pidLidVerbResponse?.Value;
}

The code is similar to reading the verb stream. Filter the extended property and include it in the response.

0
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x