Twilio SendGrid: Send Bulk Email

Twilio SendGrid: メール一斉送信

This item sends bulk email to the contacts in the specified lists and/or segments on SendGrid.

Auto Step icon
Basic Configs
Step Name
Note
Configs for this Auto Step
conf_Auth
C1: Authorization Setting in which API Key is set as token *
conf_SenderId
C2: Sender ID *
conf_SendAt
C3: Scheduled Datetime (if blank, sent immediately)
conf_ListIds
C4-A: List IDs to send the emails to (write one per line)
conf_SegmentIds
C4-B: Segment IDs to send the emails to (write one per line)
conf_UnsubscribeGroupId
C5: Unsubscribe Group ID *
conf_DesignId
C6-A: Design ID
conf_HasUniqueContent
C6-B: Configure the subject and content without using Design
conf_Subject
C6-B1: Subject *#{EL}
conf_HtmlContent
C6-B2: HTML Content *#{EL}
conf_PlainContent
C6-B3: Plain Text Content (if blank, auto-generated from HTML)#{EL}
conf_Categories
C7: Categories for filtering logs (write one per line)
conf_SingleSendId
C8: Data item to save ID of the email sending
conf_SingleSendUrl
C9: Data item to save URL of the email sending status page

Notes

  • Sender ID, List ID, Segment ID, and Design ID are contained in the URLs
    • app.sendgrid.com/settings/sender_auth/senders/(Sender ID)/edit
    • mc.sendgrid.com/contacts/lists/(List ID)
    • mc.sendgrid.com/contacts/segments/(Segment ID)
    • mc.sendgrid.com/design-library/your-designs/(Design ID)/preview
  • When you specify both List IDs and Segment IDs in the “Send To” tab, the email will be sent to all the contacts in the Lists and the Segments

Capture

See Also

Script (click to open)
  • An XML file that contains the code below is available to download
    • sendgrid-email-send-bulk.xml (C) Questetra, Inc. (MIT License)
    • If you are using Professional, you can modify the contents of this file and use it as your own add-on auto step


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 categories = retrieveCategories();
    const singleSendIdDef = configs.getObject('conf_SingleSendId');
    const singleSendUrlDef = configs.getObject('conf_SingleSendUrl');

    //// == Calculating / 演算 ==
    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 = retrieveIdsAsList('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 integer.`;
    }
    return parseInt(idStr, 10);
}

/**
  * config に設定された宛先情報を読み出す
  * @return {Object} sendTo
  * @return {Array<String>} sendTo.listIds
  * @return {Array<String>} sendTo.segmentIds
  */
function retrieveSendTo() {
    const listIds = retrieveIdsAsList('conf_ListIds', 'List IDs', MAX_LIST_ID_NUM);
    const segmentIds = retrieveIdsAsList('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 retrieveIdsAsList(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.';
    }
}

/**
  * 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 bloggers like this: