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