- GAS版のメール大量送信ツールの改良版の検討
- ドライブの中に格納する添付ファイルについて
- スプレッドシートの構成
- 一括で少しずつ宛名や内容の異なる大量のメールを作成するGASのコード
- 自作共通関数のご紹介
- 自作共通関数のメリット
- f_arr_colNameToIndex(tarArr, tarColName)
- f_arr_colNameToColArr(tarArr, tarColIndex, tarColumnsCount)
- f_arr_arrToSh(tarArr, tarSheet, tarRow, tarColumn)
- f_drive_getFileObjInChildFolder(tarChildFolderName)
- f_drive_getChildFolderId(tarChildFolderName)
- f_sh_shToArr(tarSheet, columnsCount)
- f_sh_getTarLastRow(sheet)
- f_sh_getTarLastColumn(sheet)
GAS版のメール大量送信ツールの改良版の検討
以前にこちらの記事で、GASを使用して差し込み印刷のように少しずつ内容を変えたメールをGmailで大量に送るツールをご紹介しました。
これはこちらの記事でExcelVBAでOutloookを使用してメールを送るツールのGAS版として作成したものでしたが、以下のような不便さがありました。
・スクリプトの実行時間の上限が6分(GoogleWorkSpaceだと30分)で、件数が多いと途中で止まる
多分重い添付ファイルを複数つけたメールを作成しているとGoogleWorkSpaceのアカウントでも200件いかないくらいで止まったりしました。
・途中で止まったりした場合にどこまでメールを作成していたかをスプレッドシートから判断できない
・あとは添付ファイルの入力欄をT列固定にしていたのですが、無駄なスペースが多くて見にくい問題もありました。
・そのほか添付ファイルの参照時にシートの行のループと添付フォルダに格納されているファイルを参照するループの二重ループになっていたものを一回のループで済むようにして高速化・効率化しました。
今回は上記の課題を解決したコードを作成しましたのでご紹介します。
また、これまでいくつかGASでツールを作ってきて、よく使う機能を共通化する目的で関数を複数作成して使用しています。
この辺りも使いながらご紹介していきます。
ドライブの中に格納する添付ファイルについて
こちらは以前のものと全く変わらない構成になっています。
ツールのスプレッドシートファイルと同じフォルダに「添付」フォルダを用意し、メールに添付したいファイルをここに格納します。
スポンサーリンク
スプレッドシートの構成
基本仕様
用意したスプレッドシートは以下の画像のような構成になっています。
基本的には以前の記事とほぼ変わらない作りになっていますが若干変更を加えました。
B2セルに件名、B3セルに本文のテンプレートを記述し、送付先によって変更したい内容は<会社名>や<宛名>のように「<○○>」の形式で記載しておきます。
6行目以降にメールを送るための情報と件名・本文内に埋め込みたい内容の一覧表(送付リスト)を作成します。
送付リストの中で6行目のカラム名に対して件名・本文のテンプレートの中で「<カラム名>」という記載になっている箇所を置き換えてメールを作成します。
画像ではメール本文の一行目の「<会社名>」の欄が作成したメールでは1通目が「株式会社〇×_1」、2通目が「株式会社▲□_2」となります。
添付ファイルの情報については部分一致で参照するかか完全一致で参照するかをコードの中で切り替えられるようにしていて、取得した添付ファイルの情報はI列にID、J列に完全なファイル名を記載する仕様になっています。
またメールの作成対象はA列が「〇」になっているものとなります。
変更内容
添付ファイルの情報を格納する列を可変にしました。
以前のものは添付内容についてはT列に入力する必要がありましたが、T列でなくても大丈夫ないようになりました。
ただ、ファイルを格納するフォルダ名・スプレッドシートのカラム名(6行目)・スクリプトの中のフォルダ名を全て揃えておく必要があり、
今回は「添付」という名前にしています。
一括で少しずつ宛名や内容の異なる大量のメールを作成するGASのコード
コードと仕様の概要
①メールを送付前に添付ファイルのID・ファイル名をリセットし、送信済みフラグを初期化する運用となります。
(スクリプト:resetCompFlgAndAttachInfo、ボタンを設置)
②スクリプトの上限が近づくと(5分経過時)以下の挙動で繰り返しすべてのメールを作成するまで処理が続きます。
・ループを抜けてスプレッドシートにメール作成済みのフラグを入力する
・まだメール未作成の行が残っている場合はトリガーで自身のスクリプトを1分後に再実行する
③メールの添付情報は最初の実行時に参照してスプレッドシートに記載しています。
別々にも実行できるようにボタンも容易しました。(inputArrachInfo・setMailFromSs)
(スクリプト:inputArrachInfo、ボタンを設置、メール作成用のスクリプト:inputArrachInfoAndSetMailFromSsの最初にも実行)
コードの紹介
// メール情報SS let tarSs = SpreadsheetApp.getActiveSpreadsheet(); let mailListSheet = tarSs.getSheetByName('mailList'); let startRowIndex = 6; // 送信するフラグの初期化と添付ファイル情報の削除 function resetCompFlgAndAttachInfo() { let mailListArr = func_getMailListArr(mailListSheet, startRowIndex); // メールの送付先・内容などの配列 let flagColIndex = f_arr_colNameToIndex(mailListArr, '送信する') // カラム名が「送信する」になっているカラムを抜き出す; let flagArr = f_arr_colNameToColArr(mailListArr, 0, 1); // 添付ファイルのカラムインデックスを取得 let attachColName = '添付'; let attachColIndex = f_arr_colNameToIndex(mailListArr, attachColName) // カラム名がattachColNameになっているカラムを抜き出す // SSから添付ファイルの記載位置を取得 let attachInfoArr = f_arr_colNameToColArr(mailListArr, attachColIndex, 3); // 添付のカラムを二次元配列に格納 for (let i = 1; i < attachInfoArr.length; i++) { // 入力先のセルを初期化 flagArr[i][0] = '〇'; // 送信するフラグの設定 attachInfoArr[i][1] = ''; // 添付ファイルIDの初期化 attachInfoArr[i][2] = ''; // 添付ファイル名の初期化 } flagArr = f_arr_arrToSh(flagArr, mailListSheet, startRowIndex, flagColIndex + 1); // 送信するフラグをSSに入力 attachInfoArr = f_arr_arrToSh(attachInfoArr, mailListSheet, startRowIndex, attachColIndex + 1); // SSの添付情報を初期化 } // 添付情報の取得とメール送信 function inputAttachInfoAndSetMailFromSs() { inputAttachInfo(); setMailFromSs_TriggerRepeat(); } // 添付ファイルをスプレッドシートに記入 function inputAttachInfo() { inputAttachInfoToSs('添付', 'partial'); } // 添付ファイルを設定するコード // 引数:対象シート・対象カラム名・perfect or partial // perfect or partial:ファイル名から対象ファイルを参照する際に、完全一致で抽出するか、部分一致で抽出するか function inputAttachInfoToSs(tarColName, matchType) { let mailListArr = func_getMailListArr(mailListSheet, startRowIndex); // メールの送付先・内容などの配列 let tarColIndex = f_arr_colNameToIndex(mailListArr, tarColName) // カラム名がtarColNameになっている let attachObj = f_drive_getFileObjInChildFolder(tarColName) // 添付ファイルの情報をSSに入力 let attachInfoArr = f_arr_colNameToColArr(mailListArr, tarColIndex, 3); // 添付のカラムを二次元配列に格納 // 行のループ for (let i = 1; i < attachInfoArr.length; i++) { // 入力先のセルを初期化 attachInfoArr[i][1] = ''; attachInfoArr[i][2] = ''; // セルに入力しておいた添付ファイルの情報を記載した値を取得 → 改行で分割して配列に格納 let partialNameArr = attachInfoArr[i][0].split('\n'); for (let x = 0; x < partialNameArr.length; x++) { // 対象ファイル名:partialNameArr[x] if (matchType == 'perfect') { // ファイル名完全一致で呼び出す場合 if (attachObj[partialNameArr[x]] != undefined) { attachInfoArr[i][1] = attachInfoArr[i][1] + '\n' + attachObj[partialNameArr[x]]; attachInfoArr[i][2] = attachInfoArr[i][2] + '\n' + partialNameArr[x]; } } else if (matchType == 'partial') { // ファイル名不完全でも呼び出す場合 for (attachInfo in attachObj) { if (attachInfo.indexOf(partialNameArr[x]) != -1) { attachInfoArr[i][1] = attachInfoArr[i][1] + '\n' + attachObj[attachInfo]; attachInfoArr[i][2] = attachInfoArr[i][2] + '\n' + attachInfo; } } } } attachInfoArr[i][1] = attachInfoArr[i][1].substring(1); // 最初の改行を削除 attachInfoArr[i][2] = attachInfoArr[i][2].substring(1); // 最初の改行を削除 } f_arr_arrToSh(attachInfoArr, mailListSheet, startRowIndex, tarColIndex + 1); //SSに入力 } // メールの作成 function setMailFromSs_TriggerRepeat() { let startTime = new Date(); // 時間計測用の開始時間取得 let functionName = arguments.callee.name; deleteTrigger(functionName); // 時間切れで設定したトリガーを削除 let mailTempArr = f_sh_shToArr(mailListSheet, 2); // SSからテンプレートの読み込み let mailFrom = mailTempArr[0][1]; // 送信元がある際は let mailSubjectTemplate = mailTempArr[1][1]; let mailBodyTemplate = mailTempArr[2][1]; let mailListArr = func_getMailListArr(mailListSheet, startRowIndex); // メールの送付先・内容などの配列 let flagArr = f_arr_colNameToColArr(mailListArr, 0) // A列部分の入力用配列 let attachColumnIndex = f_arr_colNameToIndex(mailListArr, "添付ファイルID"); // 添付ファイルIDの列インデックスを取得 // 行ごとにメール作成 for (let i = 1; i < mailListArr.length; i++) { // 配信フラグがあれば送信 if (flagArr[i][0] != '' && flagArr[i][0] != '済') { let mailSubject = mailSubjectTemplate; let mailBody = mailBodyTemplate; let mailTo = mailListArr[i][1]; let options = {}; options['cc'] = mailListArr[i][2]; options['bcc'] = mailListArr[i][3]; if (mailFrom != ''){ options['from'] = mailFrom; } // 件名と本文の追加 for (let k = 4; k < mailListArr[0].length; k++) { mailSubject = mailSubject.split("<" + mailListArr[0][k] + ">").join(mailListArr[i][k]); mailBody = mailBody.split("<" + mailListArr[0][k] + ">").join(mailListArr[i][k]); if (mailListArr[0][k] == '') { break; } } // 添付ファイルの設定 let attachFileNameArr = mailListArr[i][attachColumnIndex].split('\n'); let attachFileArr = []; for (let x = 0; x < attachFileNameArr.length; x++) { if (attachFileNameArr[x] != '') { // 空文字列なら何もしない attachFileArr.push(DriveApp.getFileById(attachFileNameArr[x]).getBlob()); } } options['attachments'] = attachFileArr; setMail(mailTo, mailSubject, mailBody, options, 'create'); // Utilities.sleep(1000 * 10); // 10秒スリープ flagArr[i][0] = "済"; } // 経過時間を取得して25分以上ならトリガーをセットして終了 let setTriggerFlg = ifTimeOverSetTrigger(functionName, startTime); if (setTriggerFlg == true) { // ステータスをシートに入力 flagArr = f_arr_arrToSh(flagArr, mailListSheet, startRowIndex, 1); return; // 終了 } } // ステータスをシートに入力 flagArr = f_arr_arrToSh(flagArr, mailListSheet, startRowIndex, 1); // 25分以内に完了したらトリガーを解除して終了 deleteTrigger(functionName); } function sendDrafts() { var drafts = GmailApp.getDrafts(); // 下書きフォルダ内の最初の下書きメッセージ for (let i = drafts.length - 1; i >= 0; i--) { //if (drafts[i].getMessage().getSubject().indexOf("7312") != -1 ) { console.log(drafts[i].getMessage().getSubject()); let msg = drafts[i].send(); Utilities.sleep(3000); //} } } function func_getMailListArr(mailListSheet, startRowIndex) { let mailListArr = f_sh_shToArr(mailListSheet, 21); // メールの送付先・内容などの読み込み // メールのリスト部分を残すために5行分削除 for (let i = 0; i < startRowIndex - 1; i++) { mailListArr.shift(); } return mailListArr; } function setMail(mailTo, mailSubject, mailBody, options, createOrSend) { options['htmlBody'] = mailBody.replace(/\n/g, '
'); //メール送信 if (createOrSend == 'create') { GmailApp.createDraft(mailTo, mailSubject, mailBody, options); //to , Subject , Body の順 } else if (createOrSend == 'send') { GmailApp.sendEmail(mailTo, mailSubject, mailBody, options); //to , Subject , Body の順 } } // 二次元配列のカラム名からインデックス番号を返す function f_arr_colNameToIndex(tarArr, tarColName) { let retColIndex; for (let k = 0; tarArr[0].length; k++) { if (tarArr[0][k] == tarColName) { retColIndex = k; break; } } return retColIndex; } // 二次元配列のカラム番号から必要なカラムを抜き出す function f_arr_colNameToColArr(tarArr, tarColIndex, tarColumnsCount) { if (tarColumnsCount == undefined){ tarColumnsCount = 1 } let retArr = []; for (let i = 0; i < tarArr.length; i++) { retArr.push([]); for (let x = 0; x< tarColumnsCount;x++){ retArr[i].push(tarArr[i][tarColIndex + x]); } } return retArr; } // 配列をシートに転記 // 一応、格納後のシートを再度配列に格納できるように配列を返すように設定 function f_arr_arrToSh(tarArr, tarSheet, tarRow, tarColumn) { tarSheet.getRange(tarRow, tarColumn, tarArr.length, tarArr[0].length).setValues(tarArr); tarArr = tarSheet.getRange(tarRow, tarColumn, tarArr.length, tarArr[0].length).getValues(); return tarArr; } // 子フォルダ名から中に格納されているファイル一覧をObj型(Key:ファイル名 Item:ファイルID)で返す function f_drive_getFileObjInChildFolder(tarChildFolderName) { let tarChildFoldeId = f_drive_getChildFolderId(tarChildFolderName); let files = DriveApp.getFolderById(tarChildFoldeId).getFiles(); let retObj = {}; while (files.hasNext()) { let file = files.next(); retObj[file.getName()] = file.getId(); } return retObj; } //子フォルダ名からフォルダIDを返す function f_drive_getChildFolderId(tarChildFolderName) { let fileId = tarSs.getId(); // このファイルのID let folderIterator = DriveApp.getFileById(fileId).getParents(); // 親フォルダを取得 let parentFolder = folderIterator.next(); // 親フォルダの最初の1つ目を取得 // let parentFolderId = parentFolder.getId(); // 親フォルダのIDを取得 // let childFolder = DriveApp.getFolderById(parentFolderId); let childFolders = parentFolder.getFolders(); //子フォルダ一覧を取得 let tarChildFolderId; while (childFolders.hasNext()) { let childFolder = childFolders.next(); let childFolderId = childFolder.getId(); let childFolderName = DriveApp.getFolderById(childFolderId).getName(); if (tarChildFolderName == childFolderName) { tarChildFolderId = childFolderId; break; } } return tarChildFolderId; } function f_sh_shToArr(tarSheet, columnsCount) { if (columnsCount == undefined){ columnsCount = f_sh_getTarLastColumn(tarSheet); } retArr = tarSheet.getRange(1, 1, f_sh_getTarLastRow(tarSheet), columnsCount).getDisplayValues(); return retArr; } //最終行取得 function f_sh_getTarLastRow(sheet) { //最終行の取得フロー開始 let tarValues = sheet.getRange('A:Z').getValues().filter(String); //A-Z列の値を全て取得 let tarValuesFilterData = []; //配列の中で空欄のものを除外して、lengthを算出・最終行取得 for (let i = 0; i < tarValues.length; i++) { if (tarValues[i].join("") != "") { //空文字列で行内のセルの値を結合 tarValuesFilterData.push(tarValues[i]); } } return tarValuesFilterData.length; //空白の要素を除いた長さを取得 } //最終列取得 function f_sh_getTarLastColumn(sheet) { // 最初の10行が空欄以外の数をカウント let tarArr = sheet.getRange("1:10").getValues().filter(String); let tarArrFilterData = []; //配列の中で空欄のものを除外して、lengthを算出・最終行取得 for (let k = 0; k < tarArr[0].length; k++) { let checkStr = ""; for (let i = 0; i < tarArr.length; i++) { checkStr = checkStr + tarArr[i][k]; } if (checkStr !=""){ tarArrFilterData.push(tarArr[0][k]); } } return tarArrFilterData.length; //空白の要素を除いた長さを取得 } // 長い時間のかかるコードに対して、途中で終了してトリガーをセット・再開するための関数 function ifTimeOverSetTrigger(functionName, startTime) { let afterTime = new Date(); let progressTime = afterTime - startTime; // 25分以上経過していたらトリガーをセット let tarMinute = 25; if (progressTime >= 1000 * 60 * tarMinute) { sendLineNotify(functionName + '_' + tarMinute + '分経過なのでトリガーをセットして終了'); setTriggerOneMinite(functionName); // 1分後にトリガーセット return true; } return false; } // 1分後に実行するトリガーの設定 function setTriggerOneMinite(functionName) { //トリガーを一度削除 let triggers = ScriptApp.getProjectTriggers(); for (let trigger of triggers) { if (trigger.getHandlerFunction() == functionName) { ScriptApp.deleteTrigger(trigger); } } // 1分後に実行 let tarTime = 1 ScriptApp.newTrigger(functionName).timeBased().after(tarTime * 60 * 1000).create(); } // トリガーを削除 function deleteTrigger(functionName) { let triggers = ScriptApp.getProjectTriggers(); for (let trigger of triggers) { if (trigger.getHandlerFunction() == functionName) { ScriptApp.deleteTrigger(trigger); } } }
自作共通関数のご紹介
自作共通関数のメリット
スプレッドシートの操作をする際に、API呼び出しの回数を減らしてコードの実行をスムーズにするため、スプレッドシートの内容を配列やオブジェクト型に格納して加工し、加工した配列・オブジェクト型変数をスプレッドシートに戻す操作を良く行います。
このため、スプレッドシート⇔配列やオブジェクト型変数を行き来するための関数を複数使用していて、上記コードでは以下の自作共通関数を使用しています。
名前の付け方は「f_○○_」の形式に統一していて、○○の部分は対象となるものを記入します。(「arr:配列、obj:オブジェクト、sh:シート」など)
f_arr_colNameToIndex(tarArr, tarColName)
二次元配列のカラム名からインデックス番号を取得する関数です。
特定の配列を参照する際に使用します。
f_arr_colNameToColArr(tarArr, tarColIndex, tarColumnsCount)
二次元配列の特定の二次元配列を抜き出して新たな二次元配列を作成する関数です。
1列でも複数列でも二次元配列を作成することもできます。
f_arr_arrToSh(tarArr, tarSheet, tarRow, tarColumn)
二次元配列をシートに格納します。
格納したシートを再度参照しやすいように二次元配列を返す形にしています。
f_drive_getFileObjInChildFolder(tarChildFolderName)
ドライブの中で子フォルダのフォルダ名から中に含まれているファイルの情報をオブジェクト型(Key:ファイル名 Item:ファイルID)で返します。
f_drive_getChildFolderId(tarChildFolderName)
ドライブの中で子フォルダ名からフォルダ名IDを返す関数です。
f_drive_getFileObjInChildFolderの関数で使用します。
f_sh_shToArr(tarSheet, columnsCount)
シートのA列から指定した分の列数を対象として二次元配列に作成して取得します。
f_sh_getTarLastRow(sheet)
スプレッドシートの最終行を取得する関数です。
フォームなどを使用していた場合最終行を取得するのがうまくいかない場合があるので空白行が出てくるまでの数をカウントします。
f_sh_getTarLastColumn(sheet)
スプレッドシートの最終列を取得する関数です。
コメント