Twilio SendGrid: Send Email

Twilio SendGrid: Send Email

Twilio SendGrid: メール送信

This item sends an email to the specified email addresses in To/Cc/Bcc, using SendGrid.

Basic Configs
Step Name
Note
Auto Step icon
Configs for this Auto Step
conf_Auth
C1: Authorization Setting in which API Key is set as token *
conf_From
C2: From Email Address (must be verified as sender on SendGrid) *
conf_FromName
C3: From Display Name
conf_ReplyTo
C4: Reply-To Email Address
conf_ReplyToName
C5: Reply-To Display Name
conf_SendAt
C6: Scheduled Datetime (if blank, sent immediately)
conf_TrackOpen
C7: Track if the email is opened
conf_To
C8: To Email Addresses (write one per line) *
conf_Cc
C9: Cc Email Addresses (write one per line)
conf_Bcc
C10: Bcc Email Addresses (write one per line)
conf_Subject
C11: Subject *#{EL}
conf_HtmlContent
C12-A1: HTML Content#{EL}
conf_InlineImages
C12-A2: Inline Images to insert to HTML Content
conf_PlainContent
C12-B: Plain Text Content#{EL}
conf_Attachments
C13: Attachments
conf_Categories
C14: Categories for filtering logs (write one per line)

Notes

  • To insert inline images to the HTML body, use img tags as shown below
    • The cid is the index number of the image in the data item, starting from 0
<img src="cid:0">
<img src="cid:1">

Capture

See Also

Script (click to open)
  • An XML file that contains the code below is available to download
    • sendgrid-email-send.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 = 255;
const MAX_RECIPIENT_NUM = 1000;
const MAX_SCHEDULABLE_HOURS = 72;

function main() {
    //// == Config Retrieving / 工程コンフィグの参照 ==
    const auth = configs.getObject('conf_Auth');
    const from = retrieveEmailAndName('conf_From');
    const replyTo = retrieveEmailAndName('conf_ReplyTo');
    const sendAt = retrieveSendAt();
    const trackOpen = configs.getObject('conf_TrackOpen');
    const sendTo = retrieveSendTo();
    const content = retrieveContent();
    const attachments = retrieveAttachments();
    const categories = retrieveCategories();

    //// == Calculating / 演算 ==
    sendMail(auth, from, replyTo, sendAt, trackOpen, sendTo, content, attachments, categories);
}

/**
  * config に設定されたメールアドレスと表示名の情報を読み出す
  * @param {String} emailConfName
  * @return {Object} obj
  * @return {String} obj.email
  * @return {String} obj.name
  */
function retrieveEmailAndName(emailConfName) {
    const email = configs.get(emailConfName);
    if (email === null || email === '') {
        return null;
    }
    const obj = { email };
    const name = configs.get(`${emailConfName}Name`);
    if (name !== null && name !== '') {
        Object.assign(obj, { name });
    }
    return obj;
}

/**
  * config に設定されたカテゴリ一覧を読み出す
  * 以下の場合はエラー
  * - 件数が多すぎる
  * - カテゴリが ASCII 文字でないものを含む
  * - 文字数が多すぎる
  * - カテゴリ指定が重複
  * @return {Array<String>} categories
  */
function retrieveCategories() {
    const categories = retrieveValuesAsList('conf_Categories');
    if (categories.length > MAX_CATEGORY_NUM) {
        throw `The maximum number of Categories is ${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 に設定された宛先情報を読み出す
  * @return {Object} sendTo
  * @return {Array<Object>} sendTo.to
  * @return {Array<Object>} sendTo.cc
  * @return {Array<Object>} sendTo.bcc
  */
function retrieveSendTo() {
    const to = retrieveValuesAsList('conf_To', true);
    if (to.length === 0) { // SendGrid の仕様で、To は必須
        throw 'At least one To Email Address is required.';
    }
    const cc = retrieveValuesAsList('conf_Cc', true);
    const bcc = retrieveValuesAsList('conf_Bcc', true);
    const recipientNum = to.length + cc.length + bcc.length;
    if (recipientNum > MAX_RECIPIENT_NUM) {
        throw `The maximum number of recipients (To + Cc + Bcc) is ${MAX_RECIPIENT_NUM}.`;
    }
    const recipients = new Set(to.map(obj => obj.email).concat(cc.map(obj => obj.email)).concat(bcc.map(obj => obj.email))); // 重複確認用
    if (recipientNum !== recipients.size) {
        throw 'Each email address must be unique between To, Cc, and Bcc.';
    }
    const sendTo = { to };
    // Cc と Bcc は、空でない場合にのみ設定する
    if (cc.length > 0) {
        Object.assign(sendTo, { cc });
    }
    if (bcc.length > 0) {
        Object.assign(sendTo, { bcc });
    }
    return sendTo;
}

/**
  * config に設定された値の一覧を読み出す
  * @param {String} confName 設定名
  * @param {boolean} returnAsEmailObj true の場合、メールアドレスと表示名のオブジェクトの配列を返す
  * @return {Array<String>|Array<Object>} ids
  */
function retrieveValuesAsList(confName, returnAsEmailObj = false) {
    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 (returnAsEmailObj) {
            return ids.map((email) => ({ email }));
        }
        return ids;
    }
    // 選択型データ項目の場合
    const selects = engine.findData(dataDef);
    if (selects === null || selects.size() === 0) {
        return [];
    }
    const ids = [];
    selects.forEach(item => {
        if (returnAsEmailObj) {
            const email = item.getValue();
            const name = item.getDisplay();
            ids.push({ email, name });
            return;
        }
        ids.push(item.getValue());
    });
    return ids;
}

/**
  * config に設定された送信日時を UNIX タイムスタンプとして読み出す
  * データ項目が選択されていない場合は文字列 null を返す
  * @return {Number} sendAt
  */
function retrieveSendAt() {
    const dataDef = configs.getObject('conf_SendAt');
    if (dataDef === null) {
        return null;
    }
    const datetime = engine.findData(dataDef); // AddableTimestamp
    if (datetime === null) {
        throw 'Scheduled Datetime is selected but its data is null.';
    }
    const sendAt = Math.floor(datetime.getTime() / 1000);
    const now = Math.floor(Date.now() / 1000);
    if (sendAt <= now) {
        throw 'Scheduled Datetime must be future.';
    }
    if (sendAt - now > MAX_SCHEDULABLE_HOURS * 60 * 60) {
        throw `Scheduled Datetime must be within ${MAX_SCHEDULABLE_HOURS} hours.`;
    }
    return sendAt;
}

/**
  * config に設定されたメールの中身の情報を読み出す
  * @return {Object} content メールの中身
  * @return {String} content.subject メールの件名
  * @return {String} content.htmlContent HTML メールの本文
  * @return {String} content.plainContent プレーンテキストメールの本文
  */
function retrieveContent() {
    const subject = configs.get('conf_Subject');
    if (subject === '') {
        throw 'Subject is blank.';
    }
    const htmlContent = configs.get('conf_HtmlContent');
    const plainContent = configs.get('conf_PlainContent');
    if (htmlContent === '' && plainContent === '') {
        throw 'HTML Content and Plain Content are blank. At least one of them is required.';
    }
    return { subject, htmlContent, plainContent };
}

/**
  * config に設定された添付ファイルの情報を読み出す
  * @return {Array<Object>} attachments
  */
function retrieveAttachments() {
    const attachments = [];
    // インライン画像
    const inlineImagesDef = configs.getObject('conf_InlineImages');
    if (inlineImagesDef !== null) {
        const qfiles = engine.findData(inlineImagesDef);
        if (qfiles !== null) {
            const varName = inlineImagesDef.getVarName();
            for (let i = 0; i < qfiles.size(); i++) {
                const qfile = qfiles.get(i);
                const contentType = qfile.getContentType();
                if (!contentType.startsWith('image/')) {
                    throw `${qfile.getName()} is not an image.`;
                }
                const obj = {
                    content: base64.encodeToString(fileRepository.readFile(qfile)),
                    filename: qfile.getName(),
                    type: contentType,
                    disposition: 'inline',
                    content_id: `${i}` // 0, 1, 2, ...
                };
                attachments.push(obj);
            }
        }
    }
    // 添付ファイル
    const attachmentsDef = configs.getObject('conf_Attachments');
    if (attachmentsDef !== null) {
        const qfiles = engine.findData(attachmentsDef);
        if (qfiles !== null) {
            qfiles.forEach(qfile => {
                const obj = {
                    content: base64.encodeToString(fileRepository.readFile(qfile)),
                    filename: qfile.getName(),
                    type: qfile.getContentType(),
                    disposition: 'attachment'
                };
                attachments.push(obj);
            });
        }
    }
    return attachments;
}

/**
  * メールを送信する
  * @param {AuthSettingWrapper} auth 認証設定
  * @param {Object} from 送信元
  * @param {Object} replyTo 返信先
  * @param {Number} sendAt 送信日時
  * @param {Boolean} trackOpen 開封検知を行うかどうか
  * @param {Object} sendTo 宛先情報
  * @param {Object} content メールの中身
  * @param {Array<Object>} attachments 添付ファイル
  * @param {Array<String>} categories カテゴリ一覧
  */
function sendMail(auth, from, replyTo, sendAt, trackOpen, sendTo, content, attachments, categories) {
    const requestBody = buildRequestBody(from, replyTo, sendAt, trackOpen, sendTo, content, attachments, categories);
    const response = httpClient.begin()
        .authSetting(auth)
        .body(JSON.stringify(requestBody), 'application/json')
        .post('https://api.sendgrid.com/v3/mail/send');
    const status = response.getStatusCode();
    if (status !== 202) {
        engine.log(response.getResponseAsString());
        throw `Failed to send mail. status: ${status}`;
    }
    // リクエスト成功時のレスポンスは空
}

/**
  * リクエストボディを作成
  * @param {Object} from 送信元
  * @param {Object} replyTo 返信先
  * @param {Number} sendAt 送信日時
  * @param {Boolean} trackOpen 開封検知を行うかどうか
  * @param {Object} sendTo 宛先情報
  * @param {Object} content メールの中身
  * @param {String} content.subject メールの件名
  * @param {String} content.htmlContent HTML メールの本文
  * @param {String} content.plainContent プレーンテキストメールの本文
  * @param {Array<Object>} attachments 添付ファイル
  * @param {Array<String>} categories カテゴリ一覧
  * @return {Object} requestBody
  */
function buildRequestBody(from, replyTo, sendAt, trackOpen, sendTo, { subject, htmlContent, plainContent }, attachments, categories) {
    const requestBody = {
        from,
        reply_to: replyTo,
        send_at: sendAt,
        tracking_settings: {
            open_tracking: {
                enable: trackOpen
            }
        },
        personalizations: [sendTo],
        subject,
        content: buildContent(plainContent, htmlContent)
    };
    if (attachments.length > 0) {
        Object.assign(requestBody, { attachments });
    }
    if (categories.length > 0) {
        Object.assign(requestBody, { categories });
    }
    return requestBody;
}

/**
  * リクエストボディの content を作成
  * @param {String} plainContent プレーンテキストメールの本文
  * @param {String} htmlContent HTML メールの本文
  * @return {Array<Object>} content
  */
function buildContent(plainContent, htmlContent) {
    const content = [];
    // プレーンテキストの本文がある場合、HTML 本文よりも先に設定する必要がある
    if (plainContent !== '') {
        content.push({
            type: 'text/plain',
            value: plainContent
        });
    }
    if (htmlContent !== '') {
        content.push({
            type: 'text/html',
            value: htmlContent
        });
    }
    return content;
}

    
Scroll to Top

Discover more from Questetra Support

Subscribe now to keep reading and get access to the full archive.

Continue reading