メディア掲載: レバテックフリーランス様のサイトで当ブログが紹介されました

Gmail差し込み一斉送信ツール(装飾・画像対応版)

  • 多数の宛先にメールを一斉送信したい
  • メール件名や本文内に、宛名などの受信者ごとに異なるテキストを差し込みたい
  • 受信者ごとに異なるファイルを添付したい

上記のようなケースで活用できる、Gmail差し込み一斉送信ツールを作成しましたので紹介します。

なお、当ブログの過去の記事で、同種のツールを公開していますが、
この記事で掲載するのは、文字の装飾(フォント、サイズ、太字・・・など)や、メール内のインライン画像にも対応したバージョンになります。

*このツールを使用したことにより、ご利用者様、または第三者に損害・トラブル等が発生した場合でも、一切の責任を負いません。自己責任の上でのご利用をお願いいたします。

* Gmailは1日に送信可能な件数などに制限があります。
  大量に送信する予定の場合は、Googleのヘルプ(Gmailの制限GASの制限)をご確認ください。

* ご所属の組織の管理者が行う設定により、ファイルの添付ができない場合があります。
 詳細はGoogle公式のヘルプをご参照ください。

ツールのダウンロード・利用準備

差込Gmailツール(装飾対応版)

上記のリンク先で、コピーを作成をクリックしてください。マイドライブにコピーが作成されます。
初めてツールを利用する時のみ、メール選択ボタン等を押して権限の確認を行なってください。

「承認が必要です」というダイアログが表示されますので、権限を確認 をクリックします。
* この部分の解説画像は、他の記事から流用しているため、実際のものと若干異なる場合があります。

このスクリプトを使うGoogleアカウントを選択します。

(この画面が表示された場合のみ)左下の 詳細 をクリックします。

(この画面が表示された場合のみ)左下の プロジェクト名(安全ではないページ)に移動 をクリックします。

右下の 許可 をクリックします。

ツールの利用方法

STEP1-1: 雛形メールの作成

まずは、Gmailの画面から雛形となる下書きメールを作成してください。

Gmailのメール作成画面。
メールの件名と本文が入力されており、氏名や面接場所など、宛先ごとにテキストを差し込みたい箇所は二重波括弧をもちいて、{{ 氏名 }} {{ 面接場所 }}のように入力している。
  • 宛先別のテキストを差し込みたい箇所には {{ 氏名 }}{{ 面接日時 }} のように、項目名を二重波括弧で囲って入力してください。
  • すべての宛先共通で送るファイルは、この下書きメールに添付してください。

STEP1-2: 雛形メールの選択

差込Gmailツールメール選択ボタンを押し、雛形メール(先ほど作成した下書き)を選択してください。

差込Gmailツール(装飾対応版)のスクリーンショット。
画面上部に「メール選択」ボタンがある。
[ メール選択 ] ボタンをクリックする
差込Gmailツール(装飾対応版)のスクリーンショット。
「メール選択」ボタンを押した後の「下書きメール選択」画面。
Gmailの下書きが表示されている。
下書き状態のメールが一覧表示されるので、雛形として使用するメールを選択する。
差込Gmailツール(装飾対応版)のスクリーンショット。
メールIDに、選択した下書きのIDが自動入力された。
メールIDが自動入力される。

STEP2: 発信者設定(オプション)

発信元のメールアドレスと、発信者名を変更することができます。
* 変更する必要がない場合は空欄にしてください。

なお、発信者Emailは、何でも指定できるわけではなく、Gmailの設定(アカウントとインポート > 名前:)で追加したメールアドレスしか利用できません。

差込Gmailツール(装飾対応版)のスクリーンショット。
サンプルとして、発信者Emailには「noreply@web-breeze.net」,発信者名には「微風 on the web...」が入力されている

STEP3: 宛先リストの作成

宛先リストシートに、宛先や差込テキスト、個別の添付ファイルを入力してください。

差込Gmailツール(装飾対応版)のスクリーンショット。
「宛先リスト」シートに3行分のデータが入力されている。
宛先リストシートに、宛先のアドレスや、差し込みするテキスト、個別の添付ファイルの情報などを入力する。

入力内容については、下表を参照してください。

項目詳細
A – C 列宛先、CC、BCCアドレスメールの宛先として指定するアドレス。
複数入力する場合はカンマで区切って入力する。
D – M 列差込項目 および 差込テキスト1行目: 差込項目の名前(例: 氏名)* 記号は使用しないでください。
2行目以降: 差込むテキスト(例: 鈴木 太郎)

例えば、D1氏名D2鈴木 太朗と入力した場合、
1通目のメールの{{ 氏名 }}の部分に鈴木 太朗が差し込まれる。
N 列個別添付ファイル格納フォルダID宛先個別の添付ファイルを送信したい場合、
そのファイルを格納しているGoogle DriveのフォルダのIDを入力。
(フォルダのIDはURLのfolders/の後に続く文字列。)

例: 下記の下線部分がフォルダのID
https://drive.google.com/drive/u/0/folders/1pAdczTqvTugL7A_bwIVoOsqFQsp_PTla
O – Q 列個別添付ファイル名1~3宛先個別の添付ファイルを送信したい場合、
そのファイルのファイル名を入力。
なお、ファイルはN列で指定したフォルダに格納しておく必要がある。
R列処理結果下書き作成 または メール送信の実行結果が入力される。
なお、「送信成功」はあくまでメールの送信が成功した状態であり、
相手に届いたことを保証するものではありません。
そのため、通常のメール同様、エラーメールが帰ってきていないか等も確認してください。

STEP4: 送信・下書き作成

STEP1 – STEP3の準備ができたら、メール送信または下書き作成を行うことができます。

差込Gmailツール(装飾対応版)のスクリーンショット。
画面下部に「プレビュー」「下書き作成」「メール送信」の3つのボタンがある。
ツール下部のボタンをクリックし処理を開始する。

また、プレビュー機能を使って、差込処理後のメールの内容を確認することもできます。

差込Gmailツール(装飾対応版)のスクリーンショット。プレビュー画面。
実際に送信されるメールのプレビューが表示されている。
プレビューウィンドウ。差し込みを反映したメールの内容を、Gmail風の画面で確認することができる。

参考

参考までに、ツールのソースコードを掲載します。お気づきの点などあればお知らせください。

const spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
const mainSheet = spreadSheet.getSheetByName('メイン');
const recipientsSheet = spreadSheet.getSheetByName('宛先リスト');
const ui = SpreadsheetApp.getUi();

/**
 * メールを一括送信する
 */
function sendEmails() {
  // メール雛形および宛先リストデータを取得
  const draftId = mainSheet.getRange('C3').getValue();
  const baseContents = getBaseContents(draftId);
  const eachContentsList = getEachContentsList();

  // 実行確認ダイアログ
  const result = ui.alert('確認', `${eachContentsList.length}件のメールを一括送信してよろしいですか?`, ui.ButtonSet.YES_NO);
  if (result == ui.Button.NO) return;

  // 送信結果欄のクリア
  recipientsSheet.getRange('R2:R').clear();

  // メール一括送信
  eachContentsList.forEach((eachContents, index) => {
    try {
      const mail = createMail(baseContents, eachContents);
      GmailApp.sendEmail(mail.to, mail.subject, mail.body, mail.options);
      recipientsSheet.getRange(index + 2, 18)
        .setValue(`${Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss")}: 送信成功`);
    } catch(error) {
      recipientsSheet.getRange(index + 2, 18)
        .setValue(`${Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss")}: 送信失敗(${error})`);
    }
  });

  // 終了メッセージ表示、宛先リストシート表示
  ui.alert('処理終了', '一括送信処理が終了しました。\n宛先リストシートの処理結果欄を確認してください。', ui.ButtonSet.OK);
  recipientsSheet.getRange("R2").activate();
}

/**
 * 下書きを一括作成する
 */
function createDrafts() {
  // メール雛形および宛先リストデータを取得
  const draftId = mainSheet.getRange('C3').getValue();
  const baseContents = getBaseContents(draftId);
  const eachContentsList = getEachContentsList();

  // 実行確認ダイアログ
  const result = ui.alert('確認', `${eachContentsList.length}件の下書きを一括作成してよろしいですか?`, ui.ButtonSet.YES_NO);
  if (result == ui.Button.NO) return;

  // 送信結果欄のクリア
  recipientsSheet.getRange('R2:R').clear();

  // 下書き一括作成
  eachContentsList.forEach((eachContents, index) => {
    try {
      const mail = createMail(baseContents, eachContents);
      GmailApp.createDraft(mail.to, mail.subject, mail.body, mail.options);
      recipientsSheet.getRange(index + 2, 18)
        .setValue(`${Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss")}: 作成成功`);
    } catch(error) {
      recipientsSheet.getRange(index + 2, 18)
        .setValue(`${Utilities.formatDate(new Date(), "JST", "yyyy-MM-dd HH:mm:ss")}: 作成失敗(${error})`);
    }
  });

  // 終了メッセージ表示、宛先リストシート表示
  ui.alert('処理終了', '一括下書き作成処理が終了しました。\n宛先リストシートの処理結果欄を確認してください。', ui.ButtonSet.OK);
  recipientsSheet.getRange("R2").activate();
}

/**
 * メール雛形を取得する
 */
function getBaseContents(messageId) {
  const draft = GmailApp.getMessageById(messageId);
  return {
    subject: draft.getSubject(),
    body: draft.getBody(),
    plainBody: draft.getPlainBody(),
    from: mainSheet.getRange('C6').getValue(),
    name: mainSheet.getRange('C7').getValue(),
    attachments: draft.getAttachments({ includeInlineImages: false }),
    inlineImages: getInlineImages(draft),
  }
}

/**
 * 下書きメール内のインライン画像を取得し、メールのオプション(inlineImages)を返す
 */
function getInlineImages(draft) {
  // 下書きメールに添付されているインライン画像を取得
  const inlineAttachments = draft.getAttachments({ includeAttachments: false })
  if (!inlineAttachments.length) return {};

  // 下書きメール本文からインラインイメージのimgタグ情報を取得
  const imageTags = (() => {
    const tags = draft.getBody().match(/\<img.*?cid.*?\>/g);    // "cid"を含むimgタグ全体を取得
    return tags.map(tag => {
      return {
        tag: tag,                               // imgタグ全体
        cid: tag.match(/src="cid:(.*?)"/)[1],   // imgタグ内のcid
        alt: tag.match(/alt="(.*?)"/)[1]        // imgタグ内のalt(=ファイル名)
      }
    })
  })();
  
  // Gmail用のinlineImagesオブジェクト { cid1: blob1, cid2: blob2, ... } を返す
  return inlineAttachments.reduce((acc, attachment) => {
    const fileName = attachment.getName();
    const cid = imageTags.find(imageTag => imageTag.alt === fileName).cid;
    return cid
      ? { ...acc, [cid]: attachment.copyBlob() }
      : acc
  }, {});
}

/**
 * 宛先リストの情報を取得する
 */
function getEachContentsList() {
  const [keys, ...records] = recipientsSheet.getDataRange().getDisplayValues();
  return records.map(record => {
    return {
      to: record[0],
      cc: record[1],
      bcc: record[2],
      replaceInstructions: getReplaceInstructions(keys.slice(3, 13), record.slice(3, 13)),
      attachmentFolderId: record[13],
      attachmentNames: record.slice(14, 17).filter(cell => cell !== '')
    };
  });
}

/**
 * 置換内容(before,after)の配列を返す
 */
function getReplaceInstructions(keys, values) {
  const instructions =  keys.map((key, index) => {
    return { before: key, after: values[index] }
  });
  return instructions.filter(instruction => instruction.before !== '');
}

/**
 * 雛形および宛先リストのレコードからGmail送信用のデータを作成する
 */
function createMail(baseContents, eachContents) {
  // 差込反映後の件名・本文を取得
  const subject = getReplacedString(baseContents.subject, eachContents.replaceInstructions);
  const htmlBody = getReplacedString(baseContents.body, eachContents.replaceInstructions);
  const body = getReplacedString(baseContents.plainBody, eachContents.replaceInstructions);
  
  // 個別添付ファイルの取得
  const eachAttachments = eachContents.attachmentNames.map(attachementName => 
    getAttachment(eachContents.attachmentFolderId, attachementName)
  );
  
  // オプションの設定
  const options = { htmlBody: htmlBody }
  if (baseContents.from) options.from = baseContents.from;
  if (baseContents.name) options.name = baseContents.name;
  if (eachContents.cc) options.cc = eachContents.cc;
  if (eachContents.bcc) options.bcc = eachContents.bcc;
  if (baseContents.attachments.length || eachAttachments.length) {
    options.attachments = [...baseContents.attachments, ...eachAttachments];
  }
  if (Object.keys(baseContents.inlineImages).length) options.inlineImages = baseContents.inlineImages;
  console.log(options)
  return { 
    to: eachContents.to, 
    subject: subject, 
    body: body, 
    options: options
  }
}

/**
 * 差込(置換)を反映したテキストを返す
 */
function getReplacedString(string, instructions) {
  return instructions.reduce((acc, instruction) => {
    const re = new RegExp(`\{\{ *${instruction.before} *\}\}`, "g");
    return acc.replace(re, instruction.after);
  }, string)
}

/**
 * フォルダID,ファイル名から添付ファイルを取得する
 */
function getAttachment(folderId, fileName) {
  const folder = (() => {
    try {
      return DriveApp.getFolderById(folderId);
    } catch {
      throw new Error(`添付ファイル格納フォルダが存在しない、または、アクセス権がありません。(フォルダID: ${folderId})`); 
    }
  })();

  const files = folder.getFilesByName(fileName);
  if (!files.hasNext()) throw new Error(`フォルダ内に「${fileName}」 が存在しません。`); 
  const file = files.next();
  if (files.hasNext()) throw new Error(`フォルダ内に「${fileName}」 が複数存在します。`);
  return file;
}

/**
 * 下書き選択ダイアログを表示する
 */
function showSelectDraftDialog() {
  const htmlOutput = HtmlService
    .createHtmlOutputFromFile('selectDraftDialog')
    .setWidth(700)
    .setHeight(500);
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, '下書きメール選択');
}

/**
 * ユーザーの下書きメールを取得する
 */
function getDrafts() {
  const drafts = GmailApp.getDraftMessages();
  if (!drafts.length) throw new Error(`下書きのメールが存在しません。`); 
  return drafts.map(draft => {
    const subject = draft.getSubject();
    return {
      id: draft.getId(),
      subject: subject ? subject : '(件名なし)',
      attachmentCount: draft.getAttachments().length,
      createdAt: Utilities.formatDate(draft.getDate(), 'JST', 'yyyy-MM-dd HH:mm'),
      body: draft.getPlainBody()
    }
  })
}

/**
 * 選択した下書きメールの情報をシートに反映する
 */
function setSelectedDraft(draft) {
  mainSheet.getRange('C3').setValue(draft.id);
}

/**
 * プレビューダイアログを表示する
 */
function showPreviewDialog() {
  const htmlOutput = HtmlService
    .createHtmlOutputFromFile('previewDialog')
    .setWidth(800)
    .setHeight(600);
  SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'プレビュー');
}

/**
 * プレビュー表示用のメール雛形と宛先リストデータを返す
 */
function getPreviewData() {
  // メール雛形および宛先リストデータを取得
  const draftId = mainSheet.getRange('C3').getValue();
  const baseContents = getBaseContents(draftId);
  const eachContentsList = getEachContentsList();

  // インライン画像をプレビュー表示用に変換
  baseContents.inlineImages = Object.keys(baseContents.inlineImages).map(cid => {
    const inlineImage = baseContents.inlineImages[cid];
    return {
      cid: cid,
      contentType: inlineImage.getContentType(),
      base64: Utilities.base64Encode(inlineImage.getBytes())
    }
  })

  // プレビュー表示用に添付ファイル名を取得
  baseContents.attachments = baseContents.attachments.map(attachment => attachment.getName());  

  return { baseContents, eachContentsList }
}
<!DOCTYPE html>
<html>
  <head>
    <style>
      .list {
        padding: 10px;
      }

      .listItem {
        padding: 10px;
        border-bottom: 1px solid lightgrey;
        height: 65px;
      }

      .listItem:hover {
        background: #f3f3f3;
        cursor: pointer;
      }

      .listItemHeader {
        display: flex;
        justify-content: space-between;
        color: grey;
        font-size: 0.8em;
      }

      .body {
        display: -webkit-box;
        -webkit-line-clamp: 1;
        -webkit-box-orient: vertical;
        overflow: hidden; 
        color: grey;
        font-size: 0.9em;
      }
    </style>
  </head>
  <body>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <div id="app">
      
      <!-- 下書きメール一覧 -->
      <div class="list" v-if="drafts.length">
        <div class="listItem" @click="handleClickDraft(draft)" v-for="draft in drafts">
          <div class="listItemHeader">
            <div style="flex-grow: 1;">ID: {{ draft.id }}</div>
            <div v-show="draft.attachmentCount">(添付ファイルあり)</div>
            <div>{{ draft.createdAt }}</div>
          </div>
          <div>{{ draft.subject }}</div>
          <div class="body">{{ draft.body }}</div>
        </div>
      </div>

      <!-- データ取得中のみ表示 -->
      <div v-else>Loading ...</div>
    </div>
    <script>
      const { createApp } = Vue;

      createApp({
        data() {
          return {
            drafts: [],
          }
        },
        mounted() {
          google.script.run.withSuccessHandler((result) => {
            this.drafts = result;
          }).withFailureHandler((error) => {
            alert(error.message);
            google.script.host.close();
          }).getDrafts();
        },
        methods: {
          handleClickDraft(draft) {
            google.script.run.setSelectedDraft(draft);
            google.script.host.close();
          }
        }
      }).mount('#app');
    </script>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <style>
      label {
        white-space: nowrap;
        margin-right: 0.5em;
      }

      .email-chips {
        display: flex;
        gap: 5px;
        flex-wrap: wrap;
      }

      .email-chip {
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 2px;
        border: 1px solid lightgrey;
        border-radius: 50vh;
        font-size: 0.9em;
      }

      .email-chip-icon {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 24px;
        height: 24px;
        border-radius: 50%;
        background: lavender;
      }

      .email-chip-address {
        margin: 0 5px;
      }

      .attachment {
        width: 500px;
        margin-block: 7px;
        padding: 7px;
        background: whitesmoke;
      }

      .pagination {
        position: fixed;
        bottom: 0;
        width: 100%;
        padding-block: 10px;
        display: flex;
        justify-content: center;
        gap: 30px;
        background: white;
        border-top: 1px solid lightgrey;
        user-select: none;
      }

      .page-input {
        width: 4em;
        text-align: center;
      }

      .prev-btn:hover,
      .next-btn:hover {
        cursor: pointer;
        text-decoration: underline;
      }
    </style>
  </head>
  <body>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <div id="app">
      <div v-if="eachContentsList.length">

        <!-- 差出人 -->
        <div style="padding-block: 7px;">
          <label>差出人</label>
          <span v-if="!selectedEmail.options.name && !selectedEmail.options.from">あなた(変更なし)</span>
          <span v-if="selectedEmail.options.name">{{ selectedEmail.options.name }}</span>
          <span v-if="selectedEmail.options.from"> <{{ selectedEmail.options.from }}></span>
        </div>

        <!-- 宛先 -->
        <div style="padding-block: 7px; display: flex;">
          <label>宛先</label>
          <div class="email-chips">
            <div class="email-chip" v-for="address in csvToArray(selectedEmail.to)">
              <div class="email-chip-icon">{{ address.slice(0 ,1) }}</div>
              <div class="email-chip-address">{{ address }}</div>
            </div>
          </div>
        </div>

        <!-- cc -->
        <div style="padding-block: 7px; display: flex;" v-if="selectedEmail.options.hasOwnProperty('cc')">
          <label>cc</label>
          <div class="email-chips">
            <div class="email-chip" v-for="address in csvToArray(selectedEmail.options.cc)">
              <div class="email-chip-icon">{{ address.slice(0 ,1) }}</div>
              <div class="email-chip-address">{{ address }}</div>
            </div>
          </div>
        </div>

        <!-- bcc -->
        <div style="padding-block: 7px; display: flex;" v-if="selectedEmail.options.hasOwnProperty('bcc')">
          <label>bcc</label>
          <div class="email-chips">
            <div class="email-chip" v-for="address in csvToArray(selectedEmail.options.bcc)">
              <div class="email-chip-icon">{{ address.slice(0 ,1) }}</div>
              <div class="email-chip-address">{{ address }}</div>
            </div>
          </div>
        </div>

        <!-- 件名 -->
        <div style="padding-block: 7px; display: flex; align-items: center; border-top: 1px solid lightgrey; ">
          <div>{{ selectedEmail.subject }}</div>
        </div>

        <!-- 本文 -->
        <div style="padding-block: 7px; border-top: 1px solid lightgrey" v-html="selectedEmail.options.htmlBody"></div>

        <!-- 添付ファイル名 -->
        <div style="margin-bottom: 40px; padding-block: 7px;">
          <template v-if="selectedEmail.options.hasOwnProperty('attachments')">
            <div class="attachment" v-for="attachment in selectedEmail.options.attachments">{{ attachment }}</div>
          </template>
        </div>

        <!-- ページ選択 -->
        <div class="pagination">
          <div class="prev-btn" @click="() => { if(page > 1) page -= 1 }">< prev</div>
          <input type="number" min="1" :max="eachContentsList.length" class="page-input" v-model="page"></input>
          of {{ eachContentsList.length }}
          <div class="next-btn" @click="() => { if(page < eachContentsList.length) page += 1 }">next ></div>
        </div>
      </div>

      <!-- データ取得中のみ表示 -->
      <div v-else>Loading ...</div>
    </div>
    <script>
      const { createApp } = Vue;

      createApp({
        data() {
          return {
            baseContents: {},
            eachContentsList: [],
            page: 1,
          }
        },
        computed: {
          /**
           * 現在選択されているメール
           */
          selectedEmail() {
            const eachContents = this.eachContentsList[this.page - 1];
            // 差込反映後の件名・本文を取得
            const subject = this.getReplacedString(this.baseContents.subject, eachContents.replaceInstructions);
            const htmlBody = this.getReplacedString(this.baseContents.body, eachContents.replaceInstructions);
            const body = this.getReplacedString(this.baseContents.plainBody, eachContents.replaceInstructions);
                        
            // オプションの設定
            const options = { htmlBody: htmlBody };
            if (this.baseContents.from) options.from = this.baseContents.from;
            if (this.baseContents.name) options.name = this.baseContents.name;
            if (eachContents.cc) options.cc = eachContents.cc;
            if (eachContents.bcc) options.bcc = eachContents.bcc;
            if (this.baseContents.attachments.length || eachContents.attachmentNames.length) {
              options.attachments = [...this.baseContents.attachments, ...eachContents.attachmentNames];
            }

            return { 
              to: eachContents.to, 
              subject: subject, 
              body: body, 
              options: options
            };
          },
        },
        mounted() {
          /**
           * プレビューに使用するデータ(下書きメールおよび宛先リスト)を取得する
           */
          google.script.run.withSuccessHandler((result) => {
            this.baseContents = result.baseContents;
            this.baseContents.body = this.getEmbeddedBody(this.baseContents.body, this.baseContents.inlineImages);
            this.eachContentsList = result.eachContentsList;
          }).withFailureHandler((error) => {
            alert(error.message);
            google.script.host.close();
          }).getPreviewData();
        },
        methods: {
          /**
           * imageタグのsrcをcidからbase64に書き換えたbodyを返す
           */
          getEmbeddedBody(body, inlineImages) {
            return inlineImages.reduce((acc, image) => {
              const re = new RegExp(`src="cid:${image.cid}"`, 'g');
              return acc.replace(re, `src=data:${image.contentType};base64,${image.base64}` )
            }, body);
          },
          /**
           * カンマ区切り文字列を配列に変換する
           */
          csvToArray(csv) {
            return csv.replace(/ /g, '').split(',');
          },
          /**
           * 差込(置換)を反映したテキストを返す
           */
          getReplacedString(string, instructions) {
            return instructions.reduce((acc, instruction) => {
              const re = new RegExp(`\{\{ *${instruction.before} *\}\}`, "g");
              return acc.replace(re, instruction.after);
            }, string);
          },
        },          
      }).mount('#app');
    </script>
  </body>
</html>

コメント

  1. 小田切まり より:

    データを利用させていただいております。
    初心者でもわかるような説明でとても助かりました。

    ひとつ質問させて頂きたいのですが、Gmailで絵文字をいれると、文字化けしてしまうのですが、それについて、修正方法をご存じでしたら、教えていただくことは可能でしょうか。

    お手数ですが、ご教示頂けますと大変助かります。
    よろしくお願い致します。

  2. SS より:

    こちらのスクリプトで、大変大きな業務改善が成せました。ありがとうございます。
    全てコピペで使用させていただいておりますが、差込項目5〜9が反映されない状況です。
    何か原因は考えられるでしょうか?
    文字間違いやスペースなどが入ってしまっていないかはcheck済みです。
    お手数おかけいたしますが、ご教示いただけますと幸いです。

    • ichi3270 ichi3270 より:

      私のテストでは差込項目5-9もうまくいくようでした。
      差し支えなければ、うまくいかないケースの差込項目名と差込の値を教えていただければ、検証させていただきます。

      • SS より:

        早急なお返事ありがとうございます。確認が遅れまして申し訳ございません。

        ⚪︎差込項目1: 氏名      値: A B
        ⚪︎差込項目2: 敬称      値: 先生
        ⚪︎差込項目3: 所属      値: xxx大学
        ⚪︎差込項目4: 差込項目4      値: 4
        ————-
        ×差込項目5: 差込項目5      値: 5
        ×差込項目6: 差込項目6      値: 6
        ×差込項目7: 差込項目7      値: 7
        ×差込項目8: 差込項目8      値: 8
        ×差込項目9: 差込項目9      値: 9
        ————-
        ⚪︎差込項目10: 差込項目10      値: !!!

        上記のように設定しておりました。
        差込項目名4-9は、テストのため項目名はそのままにしております。
        また、値もテストのため数値を入れております。

        昨日改めて試したところ、全て反映されました。

        反映がされなかった当時は、1日中試したのですが、どうしても差込項目5-9が反映されませんでした。
        もしかしたら、差込項目5には1度、部門/学部 とスラッシュを入れて入力したので、それが原因だったかもしれません。
        それから何に変更しても反映されなくなりました。

        全て反映された本日も、その後、検証のため項目名に 部門/学部 と入力してみましたが、やはり部門/学部の差込項目のみ反映されなくなり、
        その後 学部 というように/をなくしても反映されなくなりました。

        何かスプレッドシートの機嫌のようなものもあるのかと思ってしまいましたが、
        特殊文字を入れずに、毎回変更後にリフレッシュしてきちんと再読み込みをさせると反映されるので、たくさん活用させていただいております。
        本当にありがとうございます。

        • ichi3270 ichi3270 より:

          確かに、スラッシュなど一部の記号を差込項目名に指定した場合うまく差込ができなそうです。
          私も気づいておりませんでしたので、大変勉強になりました。ありがとうございました。
          いつか改修したいとは思いますが、とりあえずは記号を避けていただくのが良いかと思います。

  3. NN より:

    下書きメール選択でメールIDが反映してくれない…
    「メール選択」を押して下書きが表示されるまでに、「スクリプトが終了しました」とメッセージが出ます。下書き候補は表示され選択できるが無反応です。
    原因を教えて頂ければ幸いです。よろしくお願いします。

  4. yuri より:

    メールの一斉送信で使わせていただきたいと思いますが、ファイル添付をすると下記エラーが出て、ファイル添付ができません。(ファイル添付が無ければ、正常に動きます。)

    処理結果
     作成失敗(Error: 添付ファイル格納フォルダが存在しない、または、アクセス権がありません。(フォルダID: 1p**********************4t-))

    添付したいファイル(PDF)は、マイドライブにフォルダを作成し、リンクをコピーでID取得し、N列の「個別添付ファイル 格納フォルダID」へ貼付けています。

    リンクをコピー ※前後の【 】部分を削除
    【https://drive.google.com/drive/folders/】1p**********************4t-【?usp=sharing】

    フォルダの一般的なアクセス権は、編集者にして、会社のグループメンバーが全員検索・編集できるようにしています。
    宛先メールアドレスを自分にしてテストしています。

    装飾・画像対応版ではないコードで試したときは、エラーメッセージは、
    ≪Exception: 使用しようとしている機能は、ドメイン管理者により無効にされています。≫
    でしたので、アクセス権のエラーと思っています。

    いろいろ調べてはみたのですが、DriveApp.getFolderById(folderId) の機能を制限されているということで、会社側の設定を変更しえもらうしかないのでしょうか?
    ご教授いただけると幸いです。よろしくお願いします。

    • ichi3270 ichi3270 より:

      私も存じ上げていなかったのですが、組織の管理者側の設定で、Google Apps Script等からDrive関連の操作を許可するか否かの設定があるようで、それを変更いただくしか方法は思いつきません。
      お力になれず申し訳ありません。

      • yuri より:

        お返事いただきありがとうございます。
        ご助言いただきました内容にて、Google Apps Script等からDrive関連の操作を許可する設定をシステム管理者へ依頼し、対応してもらうことで、ファイル添付することができました。活用させていただきたいと思います。ありがとうございました。

  5. YA より:

    お世話になっております。
    とても簡単かつ親切な内容で、わたしでも設定することができ大変助かりました。
    ありがとうございます。

    ご相談なのですが、
    個別添付ファイルで画像を送付しているのですが、
    この画像をメール本文に埋め込むとこは可能でしょうか?

    色々調べてみたのですがどうしてもうまくいかず、教えていただけますと幸いです。
    よろしくお願いいたします。

  6. aa より:

    これまで、こちらのシートを何事もなく使えていたのですが、最近になってコピーしてもう一度使用しようとすると、「スクリプト showSelectDraftDialog でエラーが発生しました」というエラーが出たり、認証しているのにメール送信をクリックしても送れない時があります。原因はありますでしょうか。。?

タイトルとURLをコピーしました