ゆるおたノート

Tomorrow is another day.

【Google Apps Script × Gmail】下書きを量産するスクリプト

「メールを大量送信するので、下書きを大量に準備しておいてほしい」とのご依頼をいただきました。

曰く、「何件送信するか分からないので、とりあえず数百件作っといて!」とのことです。
スクリプトを書いて」などの指示は特にありませんでしたが、この量だと「どこかで転記ミスする未来」がハッキリと見えました。(怖)
しかも、手作業が苦手な私には…

クライアントさんに許可を頂いて、これを自動化してみました。

依頼内容

メールの書式は下記の通りです。

項目
To (空欄)
CC xxx@xxxx.xx
件名 ●●のお知らせ
本文 (太字やマーカーなど、装飾アリ)
添付ファイル PDF×2点

クライアントさんからのコメント

  • 本文はあらかじめ作成してあって、☆(スター)をつけたのでこれをコピーして使ってほしい。
  • Toの部分は、別の作業者が条件に沿ってマスター(並行して作成中)から転記して送信するので、空けておいて。

スクリプト

function Main() {
  // ★付きの下書きメールから本文をコピーする
  const STARED_NUM  = 5;
  var draftTemplate = getDraftTemplate(STARED_NUM);
  var htmlBody      = draftTemplate.getMessage().getBody();

  var options = {
    cc         : 'xxx@xxxx.xx',
    htmlBody   : htmlBody,
    // 所定のドライブに保存したファイルを添付ファイルとする
    attachments: getAttachmentsByFolderId('xxxxxxxxxxxxxxxxxxxxxxxx'),
  }
  
  // 引き数まとめ
  var args = {
    to       : '',
    title    : '●●のお知らせ',
    plainBody: '仮の本文',
    options  : options,
  };

  // 添付ファイルとあわせて下書きを作成する
  createDrafts(30, args) // 100通ずつで実行時間ギリギリ…
}

/**
 * 下書きボックスから指定した番号のメールを取り出す
 *
 * @param {number} n テンプレートとする下書きの番号(古いものからn件目)
 *
 * @return {object} 下書きオブジェクト
 */
function getDraftTemplate(n) {
  var drafts      = GmailApp.getDrafts();
  var draftCounts = drafts.length;
  
  // 最古からn番目にある、★付きの下書きメールを指定
  var targetIndex = draftCounts - n;
  var draftId     = drafts[targetIndex].getId();
  var draft       = GmailApp.getDraft(draftId);
  
  var msg = draft.getMessage().getDate();
  
  return draft;
}

/**
 * フォルダからファイルを取り出し、「Blob形式の配列」として添付ファイルをまとめる
 *
 * @param {string} folderId フォルダID
 *
 * @return {array} Blob形式データの配列
 */
function getAttachments(folderId) {
  var folder = DriveApp.getFolderById(folderId);
  var files  = folder.getFiles(); // 指定フォルダ内のイテレータを取得

  // Blob形式でファイルを配列化
  var attachments = [];
  while (files.hasNext()) {
    attachments.push(files.next());
  };
  
  return attachments;
}

/**
 * 下書きを量産する
 *
 * @param {number} max   作成する件数
 * @param {object} args .createDraftメソッドの引き数をまとめたオブジェクト
 */
function createDrafts(max, args) {
  for (var i = 0; i < max; i++) {
    GmailApp.createDraft(args.to, args.title, args.plainBody, args.options);
    
    var creationCount = i + 1;
    Logger.log('現在: ' + creationCount + ' 通目');
  }
}

補足

本文の表現

メール本文に太字やマーカー等の書式が設定されている場合や、メルマガのように文中に画像が挿入されている場合、内部的には「HTML形式」で表現されています。
Outlookの「リッチテキスト形式」なども同じはず。)

GmailMessageオブジェクトから.getBody()メソッドを呼び出すと、本文を「HTML形式」として取得できます。

const STARED_NUM  = 5;
var draftTemplate = getDraftTemplate(STARED_NUM);
var htmlBody      = draftTemplate.getMessage().getBody();

これをMain関数でoptionsオブジェクトのhtmlBodyプロパティに設定して、下書きを大量生産します。

Blobとは?

下記の変数filesに代入した時点では、まだファイルとしては実体がありません。

var folder = DriveApp.getFolderById(folderId);
var files  = folder.getFiles(); // 指定フォルダ内のイテレータを取得

ここまでだと、FileIteratorオブジェクトなる「イテレータ」を取得しただけの状態とのこと。

An iterator that allows scripts to iterate over a potentially large collection of files. File iterators can be acccessed from DriveApp or a Folder.

Class FileIterator  |  Apps Script  |  Google Developers

潜在的に巨大なファイルの集合体を、スクリプトでたどれるようにするイテレータ。「File iterator」は、DriveAppオブジェクトか、フォルダからのみアクセスできる。

イテレータと言うと「ループの処理でよく使う数値のこと?」と思いましたが、それは違うようです。(勉強中)

イテレータ(英語: iterator)とは、プログラミング言語において配列やそれに類似する集合的データ構造(コレクションあるいはコンテナ)の各要素に対する繰り返し処理の抽象化である。実際のプログラミング言語では、オブジェクトまたは文法などとして現れる。JISでは反復子(はんぷくし)と翻訳されている。

イテレータ - Wikipedia

まだちょっとよく分からないけど、GASでは「抽象化した結果、数値で表現している」ということでしょうか…?

そして話は戻りますが、ここで取得したイテレータを使ってフォルダ内のファイルを配列として格納します。

// Blob形式でファイルを配列化
var attachments = [];
while (files.hasNext()) {
  attachments.push(files.next());
};

.next()メソッドで初めて、いわゆる「ファイル」として実体を現した状態です。
その名も「Blob」*1

ただ、これもスプレッドシートやPDFなどといった保存形式ではなく、文字列や画像、色など、種類が混ざったデータも含め、コンピュータ用にすべて2進法の01だけで表現し直して、データベースに直接格納する方式だそうです。

この「Blob形式」でGmailDraftオブジェクトのプロパティに設定して、生成したメールをユーザーが開くと、元の「ファイル」として表現されるということですね。

処理が重い…

今回の処理では、1回あたり100通で実行時間ギリギリでした。
PDFファイルは数MB単位だったので、結構処理に時間がかかっている印象です。

ちゃんと検証はしていませんが、「フォルダからファイルを取得 → Blobに変換」のあたりで時間がかかっているのかなと推測しています。

なお、これを少し書き換えると、処理速度が上がるようです(後述)。

設定値の動的な取得

今回は、宛先や添付ファイルの格納フォルダなどをスクリプト内で直接指定しています。

しかし、制限時間がある中で急いで書いていたので、この後あることに気付きました。
(制限時間よりも私の頭が硬いせいな気もしますが、それは置いといて…)

例えば、下書きを「送信する直前の状態」まで編集しておいて、そこから取得する形にすれば、より正確な複製(かつ読みやすいスクリプト)が出来るはずです。
それでも値が間違えていたなら、それは下書きのせいだから…

というわけで、少し書き換えてみました。
VBAWith構文のような表現が使えたらもっとスッキリするんだろうなぁ…)

// ★付きの下書きメールをもとに複製する
const STARED_NUM  = 5;
var draftTemplate = getDraftTemplate(STARED_NUM);
var draftMessage  = draftTemplate.getMessage();

var options = {
  cc         : draftMessage.getCc(),
  htmlBody   : draftMessage.getBody(),
  attachments: draftMessage.getAttachments(),
}

// 引き数まとめ
var args = {
  to       : draftMessage.getTo(),
  title    : draftMessage.getSubject(),
  plainBody: draftMessage.getPlainBody(),
  options  : options,
};

GmailMessageオブジェクトを変数draftMessageに格納して、そこから値を引っ張るかたちです。
自分の環境で比べてみたところ、処理時間もこちらの方が速いようでした。

このことから、添付ファイルも、フォルダに格納するより直接下書きに貼り付けておくのが良さそうです。

蛇足ですが…

「確認しつつ送信」が必要であれば、今回の処理で問題無いかと思います。

ただ、もし宛先の条件がハッキリしていて一定であれば、本当は下書きの準備だけでなく送信まで一気にGASへお任せした方が良かった気がしています。
もう少し詳しくお話伺ってみれば良かったな。いま言っても後の祭りですが…

ブログ記事も「大量の」下書きの話は見つけられなくて、送信まで行うものが多いような。
需要が無いということなのかもしれませんが、目的に合わせて使い分けていただければと思います。

参考


*1:Binary Large OBject(バイナリ・ラージ・オブジェクト)の略。