Google Apps Scriptで特定のメールに対して自動アクション

Google Apps Scriptで通常メール送信はMailAppを利用しますが、それ以外のメールの操作はGmailAppを利用する必要があります(GmailAppでもメールは送信出来ます)。飛んできたメールの中身を解析して色々なアクションを行わせることが出来る為、メールを受け口にしての自動処理を構築することが可能です。

今回はこのGmailAppを利用して、飛んできたメールに対してよく利用するアクションの自動化をしてみたいと思います。

今回利用するスプレッドシート等

今回取り組むGmailAppの自動化処理は主に以下の三点。これらを今回実装してみたいと思います。

  1. 特定の条件に合致するメールの添付ファイルをドライブに自動保存する
  2. 特定の条件に合致するメールが来たらスプシに記録を取る
  3. 特定の条件に合致するメールを自動削除する

自動アクションにするメリット

GmailAppとトリガーを使っての自動処理を実装するメリットは色々と存在します。その代表的なものは以下の通り。

  • GmailはGoogle Driveとディスク容量を共用してる為、過去のメール削除でストレージ圧迫を低減出来る
  • Gmailの添付ファイルはGoogle Drive上に存在してるわけではないので、いちいち手作業で保存し直しが必要なのを自動化することが出来る。
  • メールは他のメールに埋もれがちであり、Gmail上でタスク管理をすると取りこぼしや未着手が往々にして発生する。これを防ぐことが出来る。

GmailAppはGmailそのものを操作することが出来る為、とりわけ外部フォームからのお問い合わせや特定サーバからの定例レポート受信、またこれらの過去メールの自動削除を実装することでストレージにも優しい環境を構築できます。

各機能を実装してみる

スクリプト構築の条件と注意点

今回の仕組みではメールのSubjectに【問い合わせ】という文字列が入ってるメールについて自動アクションするように仕組みを構築しています。他にも特定メアドからの・・・といったような条件を元に発動させる事が可能です。自動化する為には時間単位でのトリガーを設置して、例えば5分置きであったり、1週間単位であったりで発動させる必要があります。

タスクランナーサービスではない為、着信と同時に発火といった事はできないので数分のタイムラグは生じます。

また、Google Apps ScriptのQuota一覧を見てみると

  • 1日当たり1アカウントのメールの読み取り件数は50,000件まで
  • 1日当たりトリガー実行の合計時間は6時間まで
  • 1個当たりの添付ファイルから取得してドライブに保存するファイルサイズは最大50MBまで
  • スクリプトの連続実行時間上限は6分まで

よってこれらの制限値に抵触するとスクリプトはそこでストップしてしまいますので要注意です。特に初回実行時はこの制限値に引っ掛かりやすいです。

また、受信トレイを対象にしてしまうと目的外のものまでヒットしてしまう可能性があるので、対象となるメールはメールフィルタなどを使って特定のラベルを付けるようにし、検索ではSubjectとラベルの2つを組み合わせて処理をするようにしましょう。今回は対象とするメールには「タスク」というラベルを付けています。

※また「お問い合わせ」のような一般的なワードだと目的外のメールもヒットしてしまう可能性があるので、外部のフォームからは独特なprefixをメールのSubjectに加えておき、それを検索ワードとして利用するようにすると尚良いです。

Google Apps Scriptで6分の壁(タイムアウト)を突破する【GAS】

添付ファイルをドライブに自動保存

事前準備

今回のGmailのメッセージの添付ファイルをドライブに保存するスクリプトを使うためには事前準備が必要です。以下の内容を準備してから実行するようにします。もちろん、対象のメールのSubjectには【お問い合わせ】の文字が入ってる必要があります。

  • 対象のメールは「タスク」というラベルがついていること
  • 対象のメールは「未読」であること
  • 添付ファイルを保存するフォルダのIDを取得しておくこと

特にやりがちなのですが、人間がGmailを開いて対象のメールを開いてしまうと自動で既読になってしまいます。メールの検索量を減らす為にラベルを付けて、なおかつ自動で処理をする為に未読のものだけをターゲットにしてるので既読にならないように注意。

また、メールフィルタにて該当のメールが飛んできた場合には「受信トレイ」ラベルは外して、タスクラベルを付けるような設定をしておくと良いでしょう。

※ただしGmailAppは仕様上、メールの新規作成や削除は出来ても添付ファイルだけ除外といったことは出来ません

図:メールフィルタ作成事例

ソースコード

今回のプログラムとしては以下のような仕様で対象のメールからドライブに添付ファイルを保存しています。

  • searchwordでヒットするメールのみを処理対象としています
  • Gmail.searchで検索する際の引数は、検索ワード、検索結果を何件目から取得するか(通常は0)、検索結果を何件分取得するか?を数値で指定します。検索ワード以外の引数が無い場合にはヒットした内容すべてが処理対象になります。
  • ただし、startは良しとしてもmaxについては500がデフォルト最大値なので、初回は何回か回す必要性があると思います。
  • メールは親のメールにぶら下がるスレッドが存在するので、GmailApp.getMessagesForThreadsで取得した親の塊に対して実行します。
  • 対象のメールが既読の場合は処理をスルーします。(message.isUnreadで判定しています)
  • 対象のメールの添付ファイルが無い場合には処理をスルーします。(message.getAttachmentsのlengthで0だと添付ファイルナシ)
  • Subjectの頭に処理時の日付時刻をgetdateman関数で付け足しています。これを保存先フォルダ内に作成するサブフォルダのフォルダ名として利用しています。
  • サブフォルダ内に添付ファイルを取り出して生成します。
  • 処理が終わったものについてはmarkReadをもってしてメールを自動で既読にします。
//Subjectを検索するワード
var searchword = "label:タスク subject:(お問い合わせ)";

//添付ファイル保存先のフォルダのID
var folderid = "ここに保存先のフォルダのIDを入れる";

//Gmailを検索して合致するメールの添付ファイルをドライブに保存する
function attachToDrive(){
  //検索位置を指定する
  let start = 0
  let max = 10

  //メッセージを検索(元スレッド)
  const threads = GmailApp.search(searchword, start, max);

  //メッセージを取得する
  const messages = GmailApp.getMessagesForThreads(threads);

  //保存先フォルダを取得する
  let target = DriveApp.getFolderById(folderid);

  //メッセージの塊を回して内容を取得する
  for(const thread of messages){
    //スレッド内の個別メッセージを取得する
    for(const message of thread){
      //対象のメールが未読かどうかを判定
      let unread = message.isUnread()
      
      //未読ではない場合はスルーする
      if(unread == false){
        continue;
      }

      //添付ファイルが無い場合はスルーする
      const attach = message.getAttachments();
      if(attach.length == 0){
        continue;
      }

      //メールのSubjectを取得(日付を頭につける)
      let subject = getDateman() + " " + message.getSubject();

      //対象のフォルダにサブフォルダを作成する
      let targetdir = target.createFolder(subject).getId();
      let targetman = DriveApp.getFolderById(targetdir);

      //対象のメッセージからファイルを取り出しドライブに保存する
      for(const attachment of attach){
        //targetmanに対してファイルを生成
        targetman.createFile(attachment);
      }

      //メールを既読にする
      message.markRead();
    }
  }
}

//現在日付データを整形して返す関数
function getDateman(){
    //日付を生成
    let newdate = new Date();
    let year = newdate.getFullYear();
    let month = paddingZero(newdate.getMonth() + 1);
    let date = paddingZero(newdate.getDate());
 
    //時刻を生成
    let hour = paddingZero(newdate.getHours());
    let mins = paddingZero(newdate.getMinutes());
    let seconds = paddingZero(newdate.getSeconds());
 
    //日付時刻を成形して返す
    let strDate = year + "_" + month + "_" + date + "_ " + hour + "_" + mins + "_" + seconds;
    return strDate;
}

//頭に0をつける
var paddingZero = function(n) {
    return (n < 10)  ? '0' + n : n;
};

実行結果

実際に実行すると対象のフォルダ内に新たにサブフォルダが日付付きで作成され、その中にファイルが全て取り出されます。同時に次回実行時に重複して処理をされるのを防ぐ為に、対象のメールは自動で「既読」になるように処理を入れています。

実際にはメールのmessageIdを取得しスプシで管理して、既に処理済みの場合はスルーするといったような処理をするほうが人間のミスを考慮出来るので尚良いのではないかと思います。

図:日付フォルダが作られ中にファイルが作られる

図:処理済みメールは既読にされ重複処理を防ぐ

過去メールの自動削除

事前準備

お問い合わせなどのメール等はまずもって昔のデータを検索してみるということはありません。しかし、これらのメールや添付ファイルはメールボックスに存在している以上検索対象になりますし、何よりもドライブの容量を消費します。

しかし人間がこのような作業を普段から行っているか?というと行っている人は殆どいないと思います。故に自動的に例えば

  • 1年以上前のメールを対象とする(過去1年のメールは保全しておく)
  • また、期間としては例えば2024/12/31が現在とした場合には、2022/12/31〜2023/12/31を対象範囲とするように自動設定する。(1年間分だけを対象とする)
  • スターがついていないものを対象とする
  • こちらも1回の実行で処理できる最大件数は500件までなので初回は何回か実行する必要性があると思います。

といった条件を持って、これらに該当するメールをトリガーを使って1ヶ月に1度程度実行して自動削除する仕組みを作っておくと、常にメールボックスがクリーンな状態を保てます。今回はこの条件に合致するものを削除してみたいと思います。

ソースコード

今回のプログラムとしては以下のような仕様で対象のメールをゴミ箱に移動させます。移動だけなので実際に削除されるのは30日後なので、それまではゴミ箱から復元可能です。

過去1年分よりも前の1年間分且つスターが付いていない物が対象となるので、スターがついてるものは受信トレイに存在しつづけます。

//過去1年のメールを削除する
function deleteMailOlder1y() {
  //現在の日付から1年前および2年前の日付を算出して検索値とする
  let kensaku = pastDateGene();
  
  //検索ワードを元に検索対象のメールリストを取得する(最大500件) 
  let delthreads = GmailApp.search(kensaku,0,500);

  //delthreadsよりデータを削除実行
  for(let i = 0; i < delthreads.length; i++) {
    //対象のメールをゴミ箱に捨てる
    delthreads[i].moveToTrash();
  }
}

//1年前および2年前の日付を算出して整形して返す
function pastDateGene(){
    //今日の日付を生成
    let newdate = new Date();

    //1年前の日付を生成する
    newdate.setFullYear(newdate.getFullYear() -1);
    let startpoint = getDateman2(newdate);
    
    //2年前の日付を生成する
    newdate.setFullYear(newdate.getFullYear() -1);
    let endpoint = getDateman2(newdate);
  
    //2つの値から検索値を生成する(スター付きは除外する)
    let kensaku = `after:${endpoint} before:${startpoint} -is:starred`
    
    //検索値を返す
    return kensaku;  
}

//現在日付データを整形して返す関数
function getDateman2(datevalue){
    //日付を生成
    let newdate = new Date(datevalue);
    let year = newdate.getFullYear();
    let month = paddingZero(newdate.getMonth() + 1);
    let date = paddingZero(newdate.getDate());

    //日付時刻を成形して返す
    let strDate = year + "/" + month + "/" + date;
    return strDate;
}

//頭に0をつける
var paddingZero = function(n) {
    return (n < 10)  ? '0' + n : n;
};

メールの内容をエクスポートする

事前準備

メールは埋もれやすいだけでなく思わぬミスで削除してしまったりするケースがあります。特にスターを付けたようなメールについてはGmail上に置いておくだけでは不安なケースも。そういった場合手動でEML形式のファイルとしてエクスポートが出来るようになっています。

単体のメールのファイルなのでローカルのNASであったり、Google Drive上に保存しておくことが出来、またこのファイルはローカルならばThunderbirdのようなメーラーで閲覧する事が可能です(Drive上では標準で見ることが出来ませんが、Gmail上ではemlファイルは普通に見ることが可能です)。

Google Drive上で閲覧したい場合はサードパーティのアドオンになりますが、Letter Openerというアドオンを使うことで直接閲覧することが可能になります。ちなみにDriveの検索ではEML内部の本文検索まで対応していますので検索では引っ掛かります。

図:Letter Openerで開いてみた様子

図:Thunderbirdで開いてみた様子

MBOX形式のメールをThunderbirdを使ってOutlookにお引っ越し

ソースコード

今回のプログラムとしては以下のような仕様で対象メールをEML形式に変換してドライブに格納します。

  • タスクというラベルがついてるメールが処理対象になります。
  • 尚且つスターがついてるメールが処理対象になります。
  • メールのサブジェクトをEMLファイルのファイル名として利用する。
  • メッセージのgetRawContentで取得した内容をUtilities.newBlobにて本文、message/rfc822、ファイル名を引数で指定するとEMLファイルに変換出来る。

これでドライブにメールが単体のEMLファイルとして変換することが出来ます。

//出力先フォルダのID
let target = "ファイル出力先のフォルダのIDを入れる"

//EML形式でメールをエクスポートする
function exportEmlFile() {
  //メッセージを検索する
  let threads = GmailApp.search("label:タスク is:starred");  
  GmailApp.markThreadsRead(threads); 

  //スレッドの塊を回す
  for (let i=0; i< threads.length; i++) {
    //メッセージの塊を取り出す
    let messages = threads[i].getMessages();

    //メッセージを回す
    for (let j=0; j< messages.length; j++) {
      //個別のメッセージから値を取り出す
      let myID = messages[j].getId();           //messasgeId
      let msg = GmailApp.getMessageById(myID);
      let msgRaw = msg.getRawContent();         //メッセージ本文等
      let msgname = messages[j].getSubject();   //サブジェクトを取得する

      //EML形式のBlobとして生成する
      let msgBlob = Utilities.newBlob(msgRaw, 'message/rfc822', msgname + '.eml');

      //ファイルを生成する
      DriveApp.getFolderById(target).createFile(msgBlob);                      
    }
  }   
}

実行結果

実行するとEML形式でメールの内容が単体のファイル化されて保存されます。このファイルをメールで自身に転送しても良いですし、Drive上に保存でも良いですが、いずれのケースでも検索で全文検索でヒットしますので、利便性が落ちること無く保全することが出来ます。

図:EMLファイルが生成されました

フォーム内容をスプシに記録する

概要

外部フォームなどからの問い合わせが指定のメール宛に届き、それを担当者がGmail上でタスク管理して処理するといった事があるかと思います。ただこのやり方の大きな問題点は、前述にもあるようにメールは埋もれる・忘れる・取りこぼすということが頻繁に起きる点。メーラー上でタスク管理をしてはいけませんは鉄板のルールです。

そこで特定の条件に合致するメールについては自動でスプシに記録し、スプシ上でタスク管理を行うようにしたい場合、Gmailの自動化が必要になります。

今回の条件は以下の通り

  • 対象のメールは「タスク」ラベルが自動で付く
  • 対象のメールはスターがついてるメール
  • メール本文は以下のような内容のものが届きます。他の文字列は存在しない。
    【日付】 
    2024/12/15 
    
    【氏名】 
    山田太郎
    
    【メールアドレス】 
    hogehoge@test.com
    
    【問い合わせ内容】 
    御社の商品から水漏れします。どうにかしてください。
  • メールタイトルには「お問い合わせ」が含まれている
  • これらはメールフィルタによって自動でフィルタリングしてある

ソースコード

基本的にはこれまでのコードと同じ様なコードですが、以下のような独特の処理を行っています。

  • メール受信日から指定のtasklimit日数後を期限として日付を算出しています。(処理期限日としてる)
  • 同じメールのIDがスプシ上にあった場合には処理をスルーしています。
  • メッセージの日付やFrom, Bodyなどを個別に取得しています。
  • BodyについてはBRタグは\nの改行コードに変換し、それ以外のHTMLタグは正規表現で除外しています。
  • またメール本文についてはさらに正規表現を使って、氏名とお問い合わせ内容の項目に合致するものを抜き出しています。
  • 一連の情報を配列にし、スプシにappendRowでデータを追加しています。

これらを実現する為の補助関数として、getTaskLimit、convertHTMLTags、bodymsgPickupといった関数を用意しています。単にメール情報だけでなく、本文を分解して取り出すには正規表現が必要であるため、このような処理を構築しています。

//検索するワード
var taskman = "label:タスク subject:(お問い合わせ) is:starred";

//タスク期限
var tasklimit = 7;    //n日後をリミットとする

//メール検索してタスクラベルの付いた問い合わせでスターのあるものをスプシに記録
function mail2spread() {
  //メッセージを検索する
  let threads = GmailApp.search(taskman);  
  GmailApp.markThreadsRead(threads); 

  //スプレッドシートを取得する
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName("taskman");
  let data = sheet.getRange("A2:I").getValues();

  //スレッドの塊を回す
  for (let i=0; i< threads.length; i++) {
    //メッセージの塊を取り出す
    let messages = threads[i].getMessages();

    //メッセージを回す
    for (let j = 0; j< messages.length; j++) {
      //メッセージIDを取得する
      let msgid = messages[j].getId();           //messasgeIdを取得する

      //スプシに対象のメッセージIDがあったらスルーする
      let matchflg = false;
      for(let k = 0;k<data.length;k++){
        //レコードを一個取り出す
        let rec = data[k];

        //空行の場合すぐに終了する
        if(rec[0] == "" || rec[0] == undefined){
          break;
        }

        //メッセージIDに合致するか?
        if(rec[0] == msgid){
          matchflg = true;
          break;
        }
      }

      //フラグ判定
      if(matchflg == true){
        //処理をスルーして次の処理へ移る
        console.log("スルー");
        continue;
      }

      //個別のメッセージから値を取り出す
      let dateman = messages[j].getDate();      //メッセージの受信日付を取得する
      let subject = messages[j].getSubject();   //サブジェクトを取得する
      let fromaddr = messages[j].getFrom();     //Fromアドレス
      let toaddr = messages[j].getTo();         //Toアドレス
      let ccaddr = messages[j].getCc();         //Ccアドレス

      //メッセージ本文はHTMLタグは除外する
      let msgbody = convertHTMLTags(messages[j].getBody());      //メッセージ本文

      //msgbodyから特定文字列だけを抜き出す
      let tempbody = bodymsgPickup(msgbody);
      let fullname = tempbody[0];
      let quest = tempbody[1];

      //タスクリミットを取得する
      let tasklimit = getTaskLimit(dateman)     //n日後の日付を取得

      //書き込み用配列を用意する
      let temparr = [
        msgid,dateman,subject,fromaddr,toaddr,ccaddr,msgbody,fullname,quest,tasklimit,false
      ];
 
      //スプシに書き込みをする
      sheet.appendRow(temparr);
    }
  }  
}

//タスクリミットを算出する
function getTaskLimit(datevalue){
  //日付を取得する
  let tempdate = new Date(datevalue);

  //n日後の日付をセットする
  tempdate.setDate(tempdate.getDate() + tasklimit);

  //値を返す
  return tempdate;
}

//メール本文からHTMLタグを除外し改行コードを変換する
function convertHTMLTags(mailbody){
  //BRタグを改行コードに変換
  let str = mailbody.replace(/<br\s*\/?>/gi, '\n');

  //HTMLタグを除外する
  str = str.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'')

  //変換結果を返す
  return str;
}

//メッセージ本文から特定文字列を抜き出す
function bodymsgPickup(inputText){
  //氏名を取得する正規表現
  const nameMatch = inputText.match(/【氏名】\s*([\S\s]+?)\s*(?=【|$)/);

  //氏名が見つかった場合に取得
  const fullName = nameMatch ? nameMatch[1].trim() : null;

  //問い合わせ内容を取得する正規表現
  const questMatch = inputText.match(/【問い合わせ内容】\s*([\S\s]+?)\s*(?=【|$)/);

  //問い合わせ内容を取得
  const quest = questMatch ? questMatch[1].trim() : null;

  //配列にする
  let temparr = [fullName,quest]

  //値を返す
  return temparr;
}

実行結果

上記のコードでmail2spread関数をトリガーで実行すると、重複せずにメッセージからスプシにタスクとして登録が可能です。ただし通常フォームから送られてくるFromはフォームの持ってる固定のメアドであるケースが多い為、フォームを送ってる人のメアドは入力してもらい、それを拾うようにしたほうが良いでしょう。

そしてフォーム固有のメアドを持ってしてメールフィルタを作成しタスクラベルをつけてあげれば処理対象に繋げることが可能です。またメール本文からメアドを取り出す正規表現のコードは以下のようなものになります。

const emailMatch = inputText.match(/【メールアドレス】\s*([\w.-]+@[\w.-]+\.[a-zA-Z]+)/);

// メールアドレスが見つかった場合に取得
const emailAddress = emailMatch ? emailMatch[1] : null;

図:メール情報と本文が分解されて記録される

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)