How do you send emails with voting buttons using the Graph API? I know how to do this using EWS, but I can't find anything describing how to do this using the Graph API.
I've included all the code to send emails with voting buttons using the MS Graph API Java SDK. The voting button labels are sent in a SingleValueLegacyExtendedProperty.
// send an email
Message message = new Message();
message.subject = "Meet for lunch?";
ItemBody body = new ItemBody();
body.contentType = BodyType.TEXT;
body.content = "The new cafeteria is open.";
message.body = body;
EmailAddress emailAddress = new EmailAddress();
emailAddress.address = to;
Recipient toRecipient = new Recipient();
toRecipient.emailAddress = emailAddress;
message.toRecipients = List.of(toRecipient);
EmailAddress fromAddress = new EmailAddress();
fromAddress.address = sendingMailbox;
Recipient fromRecipient = new Recipient();
fromRecipient.emailAddress = fromAddress;
message.from = fromRecipient;
VotingButtonEncoder vbe = new VotingButtonEncoderImpl();
SingleValueLegacyExtendedProperty prop =
new SingleValueLegacyExtendedProperty();
prop.value = vbe.createVoteButtonsBase64String(
List.of("Yes, let's have lunch.", "No, thank you though."));
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxprops/e11cc753-cecf-4fdc-bec7-23304d12388a
prop.id = "Binary {00062008-0000-0000-C000-000000000046} Id 0x00008520";
List<com.microsoft.graph.options.Option> requestOptions =
new ArrayList<>();
String requestUrl = "https://graph.microsoft.com/v1.0/users/"
+ sendingMailbox + "/microsoft.graph.sendMail";
SingleValueLegacyExtendedPropertyCollectionRequestBuilder builder =
new SingleValueLegacyExtendedPropertyCollectionRequestBuilder(
requestUrl, graphClient, requestOptions);
List<SingleValueLegacyExtendedProperty> pageContents =
new ArrayList<>();
pageContents.add(prop);
SingleValueLegacyExtendedPropertyCollectionPage singleValueExtPropPage =
new SingleValueLegacyExtendedPropertyCollectionPage(
pageContents, builder);
message.singleValueExtendedProperties = singleValueExtPropPage;
boolean saveToSentItems = true;
graphClient.users(sendingMailbox)
.sendMail(UserSendMailParameterSet.newBuilder()
.withMessage(message)
.withSaveToSentItems(saveToSentItems).build())
.buildRequest().post();
Voting button encoder:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class VotingButtonEncoderImpl implements VotingButtonEncoder {
public static final String voteRequestPropHex = "0x00008520";
@Override
public String voteRequestId() {
return voteRequestPropHex;
}
@Override
public String createVoteButtonsBase64String(
Collection<String> voteOptions) throws DecoderException {
String hex = createVoteButtonsHexString(voteOptions);
byte[] bytes = Hex.decodeHex(hex.toCharArray());
return Base64.getEncoder().encodeToString(bytes);
}
@Override
public String createVoteButtonsHexString(Collection<String> voteOptions) {
// let's build a PidLidVerbStream!
// https://msdn.microsoft.com/en-us/library/ee218541(v=exchg.80).aspx
//
// that is a bit...dense...so how about the partial solution from
// Glen'S Exchange Blog
// http://gsexdev.blogspot.com/2015/01/sending-message-with-voting-buttons.html
// don't worry, as of 2017/12/11 the content there is totally A-OK. Just
// Make sure that the vote button strings are in the VoteOptionsExtra
// section.
List<VoteButtonHexifier> options =
new ArrayList<VoteButtonHexifier>(voteOptions.size());
for (String optionString : voteOptions) {
options.add(new VoteButtonHexifier(optionString));
}
String header = "0201";
// docs say count of VoteOption stuctures plus VoteOptionExtras
// structures, but appears to actually be the count of
// VoteOption + 4. Witness, here are the start of the binary from
// emails with 10, 9, 8 ... 1 voting options as created in
// Outlook. Look at the 4th and 5th hex digits which have been
// separated from the rest with a space. (Come to think of it,
// those extra 4 bytes are probably a null terminator for the
// string.)
//
// Email Options: one;two;three;four;five;six;seven;eight;nine;ten
// 0x8520: 0201 0e 00000000000000055265706c79084...
// Email Options: one;two;three;four;five;six;seven;eight;nine
// 0x8520: 0201 0d 00000000000000055265706c79084...
// Email Options: one;two;three;four;five;six;seven;eight
// 0x8520: 0201 0c 00000000000000055265706c79084...
// Email Options: one;two;three;four;five;six;seven
// 0x8520: 0201 0b 00000000000000055265706c79084...
// Email Options: one;two;three;four;five;six
// 0x8520: 0201 0a 00000000000000055265706c79084...
// Email Options: one;two;three;four;five
// 0x8520: 0201 09 00000000000000055265706c79084...
// Email Options: gov one;two;three;four
// 0x8520: 0201 08 00000000000000055265706c79084...
// Email Options: onebutton;twobutton;threebutton
// 0x8520: 0201 07 00000000000000055265706c79084...
// Email Options: onebutton;twobutton
// 0x8520: 0201 06 00000000000000055265706c79084...
// Email Options: one button
// 0x8520: 0201 05 00000000000000055265706c79084...
String recordCnt = intToLittleEndianString(options.size() + 4);
// not documented anywhere, but seems necessary (null terminator?)
String preReplyAllPadding = "00000000";
String replyToAllHeader =
"055265706C790849504D2E4E6F7465074D657373616765025245050000000000000000";
String replyToAllFooter =
"0000000000000002000000660000000200000001000000";
String replyToHeader =
"0C5265706C7920746F20416C6C0849504D2E4E6F7465074D657373616765025245050000000000000000";
String replyToFooter = "0000000000000002000000670000000300000002000000";
String forwardHeader =
"07466F72776172640849504D2E4E6F7465074D657373616765024657050000000000000000";
String forwardFooter = "0000000000000002000000680000000400000003000000";
String replyToFolderHeader =
"0F5265706C7920746F20466F6C6465720849504D2E506F737404506F737400050000000000000000";
String replyToFolderFooter = "00000000000000020000006C00000008000000";
// Yes, each option goes in TWICE as an ANSI string with no terminator
// https://msdn.microsoft.com/en-us/library/ee218406(v=exchg.80).aspx
StringBuilder optionsAscii = new StringBuilder(2000);
int count = 0;
for (VoteButtonHexifier option : options) {
optionsAscii.append("04000000")
// option (first time)
.append(option.getAsciiHexString())
.append("0849504D2E4E6F746500")
// option (second time)
.append(option.getAsciiHexString())
// internal2 - fixed
.append("00000000"
// internal3 - fixed
+ "00"
// fUseUSHeaders:
// 00000000 = international;
// 01000000 = us
+ "00000000"
// internal4 - fixed
+ "01000000"
// send behavior (01000000 = send immediately,
// 02000000 prompt user to edit or send)
+ "01000000"
// internal5 - fixed: int 0x00000002 (little endian)
+ "02000000"
// ID: record index, 1 based. (little endian)
+ intToLittleEndianString(count++)
// internal 6 (terminator, -1)
+ "ffffffff");
}
// voting option extra bits
String VoteOptionExtras =
"0401055200650070006C00790002520045000C5200650070006C007900200074006F00200041006C006C0002520045000746006F007200770061007200640002460057000F5200650070006C007900200074006F00200046006F006C0064006500720000";
// they are in here in UTF-16LE twice and up above in ASCII twice.
// https://msdn.microsoft.com/en-us/library/ee217598(v=exchg.80).aspx
StringBuilder optionsUtf16Le = new StringBuilder(2000);
for (VoteButtonHexifier option : options) {
// UTF-16LE option (first time)
optionsUtf16Le.append(option.getUtf16LeHexString())
// UTF-16LE option (second time)
.append(option.getUtf16LeHexString());
}
String allowReplyAllVal = "00"; // false
String allowReplyVal = "00"; // false
String allowReplyToFolderVal = "00"; // false
String allowForwardVal = "00"; // false
String VerbValue = header + recordCnt + preReplyAllPadding
+ replyToAllHeader + allowReplyAllVal + replyToAllFooter
+ replyToHeader + allowReplyVal + replyToFooter + forwardHeader
+ allowForwardVal + forwardFooter + replyToFolderHeader
+ allowReplyToFolderVal + replyToFolderFooter + optionsAscii
+ VoteOptionExtras + optionsUtf16Le;
return VerbValue;
}
public static String intToLittleEndianString(int count) {
return String.format("%08x", swapEndianOrder(count));
}
public static int swapEndianOrder(int i) {
return (i & 0xff) << 24
| (i & 0xff00) << 8
| (i & 0xff0000) >> 8
| (i >> 24) & 0xff;
}
}
You need both ASCII and little-endian UTF16 for the voting button labels:
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.apache.commons.codec.binary.Hex;
/**
* Accepts a String and produces ASCII and little-endian UTF16
* representations of those string to embed in voting buttons.
* TODO: enhance code to accept separate strings for the ASCII and UTF16
* strings to better support I18Z.
* @author Tim Perry
*/
public class VoteButtonHexifier
implements Comparable<VoteButtonHexifier> {
private String buttonLabel;
public VoteButtonHexifier(String buttonLabel) {
if (buttonLabel == null) {
throw new NullPointerException("buttonLabel may not be null");
}
this.buttonLabel = buttonLabel;
}
/**
* This will return valid ASCII characters IFF the input can be
* represented as ASCII characters. Be careful to sanitize input.
* @return the button label as the hex of UTF_8.
*/
public String getAsciiHexString() {
String buttonLabel = UnicodeCharacterUtils
.fixQuotesElipsesAndHyphens(this.buttonLabel);
String lengthHex = String.format("%02X", buttonLabel.length());
String labelHex = Hex.encodeHexString(
buttonLabel.getBytes(StandardCharsets.UTF_8));
return lengthHex + labelHex;
}
public String getUtf16LeHexString() {
String lengthHex = String.format("%02X", buttonLabel.length());
String labelUtf16_ElHex = utf16LeHex(buttonLabel);
return lengthHex + labelUtf16_ElHex;
}
private String utf16LeHex(String var) {
Charset charset = Charset.forName("UTF-16LE");
return Hex.encodeHexString(var.getBytes(charset));
}
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof VoteButtonHexifier)) {
return false;
}
VoteButtonHexifier other = (VoteButtonHexifier) o;
return buttonLabel.equals(other.buttonLabel);
}
@Override
public int hashCode() {
return buttonLabel.hashCode();
}
@Override
public int compareTo(VoteButtonHexifier o) {
if (o == null) {
return 1;
}
return buttonLabel.compareTo(o.buttonLabel);
}
}