使用MS Exchange Graph API Java SDK发送带有投票按钮的交换电子邮件

如何使用Graph API发送带有投票按钮的电子邮件?我知道如何使用EWS执行此操作,但我找不到任何描述如何使用Graph API执行此操作的内容。


解决方案

我已经包含了使用MS Graph API Java SDK发送带有投票按钮的电子邮件的所有代码。投票按钮标签在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://docs.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();

投票按钮编码器:

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

}

投票按钮标签需要ASCII和小端UTF16:

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

相关文章