Twilio SendGrid: メール一斉送信

Twilio SendGrid: Send Bulk Email

この工程は、SendGrid のリストやセグメントに含まれる宛先に、メールを一斉送信します。

Basic Configs
工程名
メモ
Auto Step icon
Configs for this Auto Step
conf_Auth
C1: API キーをトークンとして設定した認証設定 *
conf_SenderId
C2: 送信者 ID *
conf_SendAt
C3: 送信日時(指定しない場合、即座に送信されます)
conf_ListIds
C4-A: メールを送信する宛先リストの ID(文字型データ項目の場合、1 行に 1 つ)
conf_SegmentIds
C4-B: メールを送信する宛先セグメントの ID(文字型データ項目の場合、1 行に 1 つ)
conf_UnsubscribeGroupId
C5: 配信停止グループ ID *
conf_DesignId
C6-A: デザイン ID
conf_HasUniqueContent
C6-B: デザインを使用せず、件名と本文を直接指定する
conf_Subject
C6-B1: 件名 *#{EL}
conf_HtmlContent
C6-B2: HTML メールの本文 *#{EL}
conf_PlainContent
C6-B3: プレーンテキストメールの本文(指定しない場合、HTML メールの本文から自動生成されます)#{EL}
conf_Categories
C7: メール送信ログ検索用カテゴリ(文字型データ項目の場合、1 行に 1 つ)
conf_SingleSendId
C8: メール送信 ID を保存するデータ項目
conf_SingleSendUrl
C9: メール送信ステータス確認ページの URL を保存するデータ項目

Notes

  • 送信者 ID、リスト ID、セグメント ID、デザイン ID は、URL に含まれています
    • app.sendgrid.com/settings/sender_auth/senders/(送信者 ID)/edit
    • mc.sendgrid.com/contacts/lists/(リスト ID)
    • mc.sendgrid.com/contacts/segments/(セグメント ID)
    • mc.sendgrid.com/design-library/your-designs/(デザイン ID)/preview
  • [C3: 送信日時]にデータ項目を指定している場合に、入力値が過去の日時や空では実行時エラーとなります
  • 「送信先」タブでリスト ID とセグメント ID を両方指定すると、そのリストとセグメントに含まれるすべての宛先にメールが送信されます
  • [C6-A: デザイン ID]を指定した場合、そのデザインに設定されているカテゴリがメール送信ログに適用されます
  • [C7: メール送信ログ検索用カテゴリ]が設定できるのは、[C6-B: デザインを使用せず、件名と本文を直接指定する]を on にした場合のみです
    • [C7: メール送信ログ検索用カテゴリ]に入力される文字には半角英数(ASCII)のみの制限があり、それ以外は実行時エラーとなります

Capture

See Also

Script (click to open)
  • 次のスクリプトが記述されている XML ファイルをダウンロードできます
    • sendgrid-email-send-bulk.xml (C) Questetra, Inc. (MIT License)
    • Professional のワークフロー基盤では、ファイル内容を改変しオリジナルのアドオン自動工程として活用できます


const MAX_CATEGORY_NUM = 10;
const MAX_CATEGORY_LENGTH = 128;
const MAX_LIST_ID_NUM = 50;
const MAX_SEGMENT_ID_NUM = 10;

function main(){
    //// == Config Retrieving / 工程コンフィグの参照 ==
    const auth = configs.getObject('conf_Auth');
    const senderId = retrieveIdAsInt('conf_SenderId', 'Sender ID');
    const sendAt = retrieveSendAt();
    const sendTo = retrieveSendTo();
    const unsubscribeGroupId = retrieveIdAsInt('conf_UnsubscribeGroupId', 'Unsubscribe Group ID');
    const content = retrieveContent();
    const singleSendIdDef = configs.getObject('conf_SingleSendId');
    const singleSendUrlDef = configs.getObject('conf_SingleSendUrl');

    //// == Calculating / 演算 ==
    let categories;
    if (content.designId === undefined) { // デザインを使用しない場合のみ、config からカテゴリを読み出す
        categories = retrieveCategories();
    } else { // デザインを使用する場合は、デザインからカテゴリを取得する
        categories = getCategoriesOfDesign(auth, content.designId);
    }
    const singleSendId = createSingleSend(auth, categories, senderId, sendTo, unsubscribeGroupId, content);
    scheduleSingleSend(auth, singleSendId, sendAt);

    //// == Data Updating / ワークフローデータへの代入 ==
    setData(singleSendIdDef, singleSendId);
    setData(singleSendUrlDef, `https://mc.sendgrid.com/single-sends/${singleSendId}/stats`);
}

/**
  * config に設定されたカテゴリ一覧を読み出す
  * 以下の場合はエラー
  * - 件数が多すぎる
  * - カテゴリが ASCII 文字でないものを含む
  * - 文字数が多すぎる
  * - カテゴリ指定が重複
  * @return {Array<String>} categories
  */
function retrieveCategories() {
    const categories = retrieveValuesAsList('conf_Categories', 'Categories', MAX_CATEGORY_NUM);
    if (!categories.every(isAscii)) {
        throw 'Categories cannot include non-ascii characters.';
    }
    // API ドキュメントに文字数制限の記載はないが、実際には制限がある
    if (categories.some(category => category.length > MAX_CATEGORY_LENGTH)) {
        throw `Each category must be within ${MAX_CATEGORY_LENGTH} characters.`;
    }
    const set = new Set(categories);
    if (categories.length !== set.size) {
        throw 'The same category is set multiple times.';
    }
    return categories;
}

/**
  * 文字列が ASCII 文字だけで構成されているかどうか
  * @param {String} text テストする文字列
  * @return {boolean} 文字列が ASCII 文字だけで構成されているかどうか
  */
function isAscii(text) {
    const reg = new RegExp('^[\x00-\x7F]+$');
    return reg.test(text);
}

/**
  * config に設定された ID を数値として読み出す
  * @param {String} confName 設定名
  * @param {String} label エラーメッセージ用ラベル
  * @return {Number} id
  */
function retrieveIdAsInt(confName, label) {
    const idStr = retrieveId(confName, label);
    return validateIdAndReturnAsInt(idStr, label);
}

/**
  * config に設定された ID を読み出す
  * ID が設定されていない場合はエラー
  * @param {String} confName 設定名
  * @param {String} label エラーメッセージ用ラベル
  * @return {String} id
  */
function retrieveId(confName, label) {
    const idDef = configs.getObject(confName);
    if (idDef === null) { // 固定値で指定
        const id = configs.get(confName);
        if (id === null || id === '') {
            throw `${label} is blank.`;
        }
        return id;
    }
    // 文字型データ項目で指定
    if (idDef.matchDataType('STRING_TEXTFIELD')) {
        const id = engine.findData(idDef);
        if (id === null || id === '') {
            throw `${label} is blank.`;
        }
        return id;
    }
    // 選択型データ項目で指定
    const selects = engine.findData(idDef);
    if (selects === null || selects.size() === 0) {
        throw `${label} is not selected.`;
    }
    return selects.get(0).getValue();
}

/**
  * ID 文字列のバリデーションを行い、数値として返す
  * @param {String} idStr ID 文字列
  * @param {String} label エラーメッセージ用ラベル
  * @return {Number} id
  */
function validateIdAndReturnAsInt(idStr, label) {
    const reg = new RegExp('^\\d+$');
    if (!reg.test(idStr)) {
        throw `${label} must be non-negative integer.`;
    }
    return parseInt(idStr, 10);
}

/**
  * config に設定された宛先情報を読み出す
  * @return {Object} sendTo
  * @return {Array<String>} sendTo.listIds
  * @return {Array<String>} sendTo.segmentIds
  */
function retrieveSendTo() {
    const listIds = retrieveValuesAsList('conf_ListIds', 'List IDs', MAX_LIST_ID_NUM);
    const segmentIds = retrieveValuesAsList('conf_SegmentIds', 'Segment IDs', MAX_SEGMENT_ID_NUM);
    if (listIds.length === 0 && segmentIds.length === 0) {
        throw 'No List IDs or Segment IDs.';
    }
    return {listIds, segmentIds};
}

/**
  * config に設定された ID 一覧を読み出す
  * @param {String} confName 設定名
  * @param {String} label エラーメッセージ用ラベル
  * @param {Number} maxNum 最大件数
  * @return {Array<String>} ids
  */
function retrieveValuesAsList(confName, label, maxNum) {
    const dataDef = configs.getObject(confName);
    if (dataDef === null) {
        return [];
    }
    // 文字型データ項目の場合
    if (dataDef.matchDataType('STRING')) {
        const dataObj = engine.findData(dataDef);
        if (dataObj === null) {
            return [];
        }
        const ids = dataObj.split('\n')
            .filter(id => id !== '');
        if (ids.length > maxNum) {
            throw `The maximum number of ${label} is ${maxNum}.`;
        }
        return ids;
    }
    // 選択型データ項目の場合
    const selects = engine.findData(dataDef);
    if (selects === null || selects.size() === 0) {
        return [];
    }
    const ids = [];
    selects.forEach(item => {
        ids.push(item.getValue()); // 選択肢 ID を格納
    });
    if (ids.length > maxNum) {
        throw `The maximum number of ${label} is ${maxNum}.`;
    }
    return ids;
}

/**
  * config に設定された送信日時を文字列として読み出す
  * データ項目が選択されていない場合は文字列 now を返す
  * @return {String} sendAt
  */
function retrieveSendAt() {
    const dataDef = configs.getObject('conf_SendAt');
    if (dataDef === null) {
        return 'now';
    }
    const datetime = engine.findData(dataDef); // AddableTimestamp
    if (datetime === null) {
        throw 'Scheduled Datetime is selected but its data is null.';
    }
    if (datetime.getTime() <= Date.now()) {
        throw 'Scheduled Datetime must be future.';
    }
    return dateFormatter.format('UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'", datetime);
}

/**
  * config に設定されたメールの中身の情報を読み出す
  * @return {Object} content メールの中身
  * @return {String} content.designId デザイン ID
  * @return {String} content.subject メールの件名
  * @return {String} content.htmlContent HTML メールの本文
  * @return {String} content.plainContent プレーンテキストメールの本文
  */
function retrieveContent() {
    const hasUniqueContent = configs.getObject('conf_HasUniqueContent');
    if (!hasUniqueContent) { // デザイン ID を指定する場合
        const designId = retrieveId('conf_DesignId', 'Design ID');
        return {designId};
    }
    // 件名、本文を直接指定する場合
    throwErrorIfDesignIdIsSet();
    const subject = configs.get('conf_Subject');
    if (subject === '') {
        throw 'Subject is blank.';
    }
    const htmlContent = configs.get('conf_HtmlContent');
    if (htmlContent === '') {
        throw 'HTML Content is blank.'
    }
    const plainContent = configs.get('conf_PlainContent');
    return {subject, htmlContent, plainContent};
}

/**
  * config にデザイン ID が設定されている場合、エラーをスローする
  */
function throwErrorIfDesignIdIsSet() {
    const confValue = configs.get('conf_DesignId');
    if (confValue !== '') {
        throw 'Design ID is set while "Configure the subject and content without using Design" is enabled.';
    }
}

/**
  * デザインに設定されたカテゴリ一覧を取得する
  * @param {AuthSettingWrapper} auth 認証設定
  * @param {String} designId デザイン ID
  * @return {Array<String>} categories
  */
function getCategoriesOfDesign(auth, designId) {
    const response = httpClient.begin()
        .authSetting(auth)
        .get(`https://api.sendgrid.com/v3/designs/${encodeURIComponent(designId)}`);
    const status = response.getStatusCode();
    const responseStr = response.getResponseAsString();
    switch (status) {
        case 200:
            break;
        case 403:
            engine.log(responseStr);
            throw `Failed to get design. Read Access to Design Library is required for your API Key. status: ${status}`;
        default:
            engine.log(responseStr);
            throw `Failed to get design. status: ${status}`;
    }
    return JSON.parse(responseStr).categories;
}

/**
  * Single Send のドラフトを作成する
  * @param {AuthSettingWrapper} auth 認証設定
  * @param {Array<String>} categories カテゴリ一覧
  * @param {Number} senderId 送信者 ID
  * @param {Object} sendTo 宛先情報
  * @param {Array<String>} sendTo.listIds 宛先リストの ID 一覧
  * @param {Array<String>} sendTo.segmentIds 宛先セグメントの ID 一覧
  * @param {Number} unsubscribeGroupId 配信停止グループ ID
  * @param {Object} content メールの中身
  * @return {String} singleSendId
  */
function createSingleSend(auth, categories, senderId, sendTo, unsubscribeGroupId, content) {
    const requestBody = buildRequestBody(categories, senderId, sendTo, unsubscribeGroupId, content);
    const response = httpClient.begin()
        .authSetting(auth)
        .body(JSON.stringify(requestBody), 'application/json')
        .post('https://api.sendgrid.com/v3/marketing/singlesends');
    const status = response.getStatusCode();
    const responseStr = response.getResponseAsString();
    if (status >= 300) {
        engine.log(responseStr);
        throw `Failed to create single send. status: ${status}`;
    }
    return JSON.parse(responseStr).id;
}

/**
  * リクエストボディを作成
  * @param {Array<String>} categories カテゴリ一覧
  * @param {Number} senderId 送信者 ID
  * @param {Object} sendTo 宛先情報
  * @param {Array<String>} sendTo.listIds 宛先リストの ID 一覧
  * @param {Array<String>} sendTo.segmentIds 宛先セグメントの ID 一覧
  * @param {Number} unsubscribeGroupId 配信停止グループ ID
  * @param {Object} content メールの中身
  * @param {String} content.designId デザイン ID
  * @param {String} content.subject メールの件名
  * @param {String} content.htmlContent HTML メールの本文
  * @param {String} content.plainContent プレーンテキストメールの本文
  * @return {Object} requestBody
  */
function buildRequestBody(categories, senderId, sendTo, unsubscribeGroupId, {designId, subject, htmlContent, plainContent}) {
    const emailConfig = {
        sender_id: senderId,
        suppression_group_id: unsubscribeGroupId
    };
    if (designId !== undefined) {
        Object.assign(emailConfig, {
            design_id: designId
        });
    } else {
        Object.assign(emailConfig, {
            subject,
            html_content: htmlContent
        });
        if (plainContent !== '') {
            Object.assign(emailConfig, {
                generate_plain_content: false,
                plain_content: plainContent
            });
        }
    }
    const requestBody = {
        name: `Questetra-m${processInstance.getProcessModelInfoId()}-p${processInstance.getProcessInstanceId()}`,
        categories,
        send_to: {
            list_ids: sendTo.listIds,
            segment_ids: sendTo.segmentIds
        },
        email_config: emailConfig
    };
    return requestBody;
}

/**
  * Single Send の送信予約をする
  * @param {AuthSettingWrapper} auth 認証設定
  * @param {String} singleSendId 送信設定の ID
  * @param {String} sendAt 送信日時
  */
function scheduleSingleSend(auth, singleSendId, sendAt) {
    const requestBody = {
        send_at: sendAt
    };
    const response = httpClient.begin()
        .authSetting(auth)
        .body(JSON.stringify(requestBody), 'application/json')
        .put(`https://api.sendgrid.com/v3/marketing/singlesends/${singleSendId}/schedule`);

    const status = response.getStatusCode();
    const responseStr = response.getResponseAsString();
    if (status >= 300) {
        engine.log(responseStr);
        throw `Failed to schedule single send. status: ${status}, singleSendId: ${singleSendId}`;
    }
}

/**
  * データ項目にデータを保存する
  * @param {DataDefinitionView} dataDef データ項目の DataDefinitionView
  * @param {Object} value 保存する値
  */
function setData(dataDef, value) {
    if (dataDef !== null) {
        engine.setData(dataDef, value);
    }
}

    
%d