Search code examples
javamicrosoft-graph-apiexchange-server

Send exchange email with voting buttons using MS Exchange Graph API Java SDK


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.


Solution

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