時間指定メール送信・送信すべきかどうかの判断(J3-05)

# 1. 入力内容をGoogle Spreadseatで操作
　フォームに入力した日時指定送信メールの内容をGoogle Spreadseatで操作する。時間指定メール送信システムを構築するにあたり，いろいろアルゴリズムは考えられるが，メール送信プログラムはタイマーによりフォーム入力とは非同期で動作させることにする。
　フォームで入力されたSpreadsheetを非同期で開いて各行にあるデータの送信時間を確認する方法では，徐々にデータが溜まってしまい処理が遅くなっていくことが予想される。そこで，フォームに入力され次第，メール送信用の別Spreadsheetを特定のフォルダ（例えばmail）内に作成し，そのファイル名を送信日時にしておく。
　非同期で起動したプログラムで，そのフォルダ内にファイルがあるファイルを確認し，送信日時に現在日時が到達していれば，送信作業を行う。送信後はそのファイルは削除する。
　フォーム入力があると，そのSpreadsheetにはフォームデータ送信と同時に最下行に付け加えられていく仕様となっている。よって，そのSpreadsheetに紐付けられたGASプログラムで，mailフォルダに作ったSpreadsheetにフォームで作成されたSpreadsheetの最下行だけをコピーしておき，メール送信に備えておく。

1. フォームで送信が行われた段階で，二重起動を防ぐためのロック等の処理をした後，フォームで作成されたSpreadsheetの最下行のデータを取得する。
1. 取得したデータを専用フォルダ「mail」内に用意した新規Spreadsheetに書き込む。
1. 一定時間（たとえば１分）ごとに実行するGASプログラムで，現在日時を取得し，保存されたメールファイルの中から，送信日時が現在時刻より古いものを抽出し送信する。
1. 送信したファイルは削除する。

　今日は，3からを行う。

## 1.1. 時間指定メール送信システムのソフトウェア構成図
* さらにGASプログラムを書き足して，メール送信プログラムを完成させる。

![画像3.png](https://qiita-image-store.s3.amazonaws.com/0/146927/79a09593-f163-28ee-1b49-837e8b4b490b.png)



* 次のテンプレートを使用せよ。

```java:テンプレート
var logID    = "**********";//手動でDocument，「log2」を新規作成しIDを調べておく
var ssID1    = "**********";//フォーム入力で作成されたSpreadsheetのID
var ssID2    = "";                                            //mailフォルダに作成されたSpreadsheetのID
var folderID = "**********";                //mailフォルダのID

function submitForm(){//フォームが送信されたら呼び出される関数，重複処理を避ける
  log = new Doc(logID);
  log.print('\n'+getDateAndTime(0)+" スクリプト開始\n");
  var lock = LockService.getScriptLock();//ロックサービスのオブジェクトを生成
  try{
    lock.waitLock(30000);//複数のフォーム送信がほぼ同時にあった時，遅い方に最大30秒待ってもらう
    log.print("他のスクリプト実行要求をロック完了，最大60秒\n");
    main(log);
  }catch(err){
    log.print("発生したエラー："+err+'\n');
  }finally{
    lock.releaseLock();//次の送信のためにロック解除
    log.print(getDateAndTime(0)+" ロック解除，次のスクリプト要求を受け付け開始\n");
  } 
}

function main(log){
  var ssSrc = new Ssheet(ssID1);//フォーム入力されたデータが入っているSpreadsheet
  var todayNow =getDateAndTime(0);//現在日時の取得

  ssID2 = createSpreadsheetInFolder(folderID, "temp");//新規作成，ファイル名はとりあえずtemp後で送信日時に変更する
  var ssDst = new Ssheet(ssID2); //最後にフォーム入力されたデータだけを記録するSpreadsheet
  
  var maxRow = ssSrc.getLastRow(0);
  var maxColumn = ssSrc.getLastColumn(0);
  log.print("最後の行"+maxRow+" 最後の列"+maxColumn+"\n");

  for(var i=1;i<=maxColumn;i++){
    var index = ssSrc.getValue(0, 1, i);
    ssDst.setValue(0, 1, i, index);
    var data = ssSrc.getValue(0, maxRow, i);
    if(index == "date"){
      var dateTmp = data;
      var date = getDateAndTime(dateTmp).split('_')[0];
      log.print(date+"\n");
      ssDst.setValue(0, 2, i, date);    
    }else if(index == "time"){ //230517修正, elseがなかった
      var timeTmp = data;
      var time = getDateAndTime(timeTmp).split('_')[1];
      log.print(time+"\n");
      ssDst.setValue(0, 2, i, time);
    }else{
      ssDst.setValue(0, 2, i, data);
    }
  }
  var fn = date + "_" + time;
  ssDst.rename(fn);
  log.print("最後に入力されたデータを新規Spreadsheetにコピー終了，ファイル名を送信日時に変更\n");
  
}

//***************************この関数を完成させる******************************
function doPeriodically(){//時間ベースで定期的に実行される関数
  log.print("doPeriodically()を実行\n");





  log.print("doPeriodically()を終了\n");
}
//***************************

//-----------------------日付や時間のための関数群
function getSerial(date,time){//日付と時間からシリアル値をゲット
  var serial = new Date(date.toString().slice(0,16)+time.toString().slice(16));
  return serial;
}

function getDateAndTime(data){// 引数がゼロなら現在日時そうでなければ指定日時をyyyymmdd_hhmmssで返す
  if(data==0) var now = new Date();
  else        var now = new Date(data);
  var year = now.getFullYear();
  var month = now.getMonth() + 1;
  var day = now.getDate();
  var hour = now.getHours();
  var min = now.getMinutes();
  var sec = now.getSeconds();
  return (""+year).slice(0,4) + ("0"+month).slice(-2) + ("0"+day).slice(-2) +'_'+ 
    ("0"+hour).slice(-2) + ("0"+min).slice(-2) + ("0"+sec).slice(-2);
}

//-----------------------ファイルの新規作成等の関数群
//フォルダID，ファイルID，ファイル名を受け取り，ファイルIDのコピーをフォルダ内に作成して，ファイルのIDを返す
function copyFileInFolder(folderID, srcID, fileName) {
  var originalFile = DriveApp.getFileById(srcID);
  var folder = DriveApp.getFolderById(folderID);
  var copiedFile = originalFile.makeCopy(fileName, folder);
  copiedFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.EDIT);//リンクからアクセスできる人は編集可能にする
  var copiedFileId = copiedFile.getId();//コピーのファイルIDをゲット
  return copiedFileId;
}
//フォルダID，ファイル名を受け取り，スプレッドシートを指定フォルダ内に新規作成しそのファイルIDを返す
function createSpreadsheetInFolder(folderID, fileName) {
  var folder = DriveApp.getFolderById(folderID);
  var newSS=SpreadsheetApp.create(fileName);
  var originalFile=DriveApp.getFileById(newSS.getId());
  var copiedFile = originalFile.makeCopy(fileName, folder);
  DriveApp.getRootFolder().removeFile(originalFile);
  var copiedFileId = copiedFile.getId();//コピーのファイルIDをゲット
  return copiedFileId;
}



//////////Ssheetクラスの定義開始（コンストラクタとメンバ関数で構成）
//Ssheetクラスのコンストラクタの記述
Ssheet = function(id){
  this.ssFile = SpreadsheetApp.openById(id);
  this.ssFileName = this.ssFile.getName();
  SpreadsheetApp.setActiveSpreadsheet(this.ssFile);//値を返さない
  this.activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
}
//Ssheetクラスのメンバ関数（メソッド）の定義開始
//spreadsheetのファイル名を返すメソッド
Ssheet.prototype.getFileName = function(){
  return this.ssFileName;
}
//spreadsheetのファイル名を変更するメソッド
Ssheet.prototype.rename = function(newName){
  this.ssFile.rename(newName);
}
//spreadsheetのsheetでrow行cal列にデータを入れるメソッド
Ssheet.prototype.setValue = function(sheet,row,col,value){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  var cell = this.activeSheet.getRange(row,col);
  cell.setValue(value);
}
//spreadsheetのsheetでrow行cal列をクリアするメソッド
Ssheet.prototype.clear = function(sheet,row,col,value){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  var cell = this.activeSheet.getRange(row,col);
  cell.clear(value);
}
//spreadsheetのsheetからrow行のcal列のデータをもらってくるメソッド
Ssheet.prototype.getValue = function(sheet,row,col) {
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  var value = this.activeSheet.getRange(row, col).getValue();
  return value;
}
//背景の色を設定するメソッド
Ssheet.prototype.setBackgroundColor = function(sheet,row,col, r,g,b) {
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  var cell = this.activeSheet.getRange(row,col);
  cell.setBackgroundRGB(r,g,b);
}
//spreadsheetのsheet数を指定の数増やすメソッド
Ssheet.prototype.insertSheet = function(num){
  var sheetNum = this.activeSpreadsheet.getNumSheets();
  while(num>sheetNum){
    this.activeSpreadsheet.insertSheet();
    sheetNum++;
  }
}
//spreadsheetの指定sheetを削除
Ssheet.prototype.deleteSheet = function(sheet){
  this.activeSpreadsheet.deleteSheet(this.activeSpreadsheet.getSheets()[sheet])
}
//spreadsheetのsheetの名前をセットするメソッド
Ssheet.prototype.renameSheet = function(sheet,newName){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  this.activeSheet.setName(newName);
}
//spreadsheetの指定sheetの全データを取得
Ssheet.prototype.getValues = function(sheet){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  return this.activeSheet.getDataRange().getValues();//シートの全データを取得
}  
//spreadsheetの指定sheetを取得
Ssheet.prototype.getSheet = function(sheet){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  return this.activeSheet;//シートを返す
}
//spreadsheetの指定sheetの最後の行番号を取得
Ssheet.prototype.getLastRow = function(sheet){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  return this.activeSheet.getLastRow();//最後の行番号を取得
}
//spreadsheetの指定sheetの最後の列番号を取得
Ssheet.prototype.getLastColumn = function(sheet){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  return this.activeSheet.getLastColumn();//最後の列番号を取得
}
//spreadsheetの指定行rowを削除
Ssheet.prototype.deleteRow = function(sheet,row){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  this.activeSheet.deleteRow(row);//行を削除
}
//spreadsheetの指定行rowを挿入
Ssheet.prototype.insertRow = function(sheet,row){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  this.activeSheet.insertRows(row);//行を挿入
}
//spreadsheetの指定列colを削除
Ssheet.prototype.deleteColumn = function(sheet,col){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  this.activeSheet.deleteColumn(col);//列を削除
}
//spreadsheetの指定列colを挿入
Ssheet.prototype.insertColumn = function(sheet,col){
  this.activeSheet = this.activeSpreadsheet.getSheets()[sheet];
  this.activeSheet.insertColumns(col);//列を挿入
}
//////////Ssheetクラスの定義終了




//////////Docクラスの定義開始（コンストラクタとメンバ関数で構成）
//Docクラスのコンストラクタの記述
Doc = function(id){
  this.ID = id;
  this.doc = DocumentApp.openById(this.ID); 
  this.body = this.doc.getBody();
  this.docText = this.body.editAsText();
}
//Docクラスのメンバ関数の定義開始
//メソッドprintの定義，テキスト追加
Doc.prototype.print = function(str){
  this.docText.appendText(str);
}
//メソッドreplaceの定義，文字列置き換え
Doc.prototype.replace = function(src,dst){
  this.body.replaceText(src,dst);
}
//メソッドclearの定義，全消去
Doc.prototype.clear = function(){
  this.body.clear();
}
//メソッドgetIDの定義，ファイルIDを返す
Doc.prototype.getID = function(){
  return this.ID;
}
//指定秒数のウェイト，表示動作を遅らせたい時などに使用
Doc.prototype.waitSec = function(sec){
  var start = new Date().getSeconds();
  while((new Date().getSeconds()-start) < sec);
}
//指定ミリ秒のウェイト，表示動作を遅らせたい時などに使用
Doc.prototype.waitMiliSec = function(msec){
  var start = new Date(); //new Date()は，「1970年1月1日午前0時」からの通算ミリ秒を返す
  while((new Date()-start) < msec);
}
//今現在の日時を表示
Doc.prototype.printTodayNow = function(){
  var now = new Date();
  var year = now.getFullYear();
  var month = now.getMonth() + 1;
  var day = now.getDate();
  var hour = now.getHours();
  var min = now.getMinutes();
  var sec = now.getSeconds();
  this.docText.appendText(year +'_'+ ("0"+month).slice(-2) +'_'+ ("0"+day).slice(-2) +' '+ 
    ("0"+hour).slice(-2) +'-'+ ("0"+min).slice(-2) +'-'+ ("0"+sec).slice(-2));
}
/////////Docクラスの定義終了
```

__＜課題9＞__
 __function doPeriodically()の中身に次のプログラムを作れ。採点上，ログはわかりやすく示すこと。__

* mailフォルダを検索して，ファイルが一つでもあれば，ファイルを取得。
* ファイル名をゲット。
* ファイルIDをゲット（getID()メソッドを使う，ネットで調べて使用せよ）
* ファイル名をログに書く。
* 現在日時と送信日時をログに書く。
* 現在日時と送信日時を比較して，送信するかしないか，ログに書く。
* 送信する場合，送信先アドレスから，送信する本文まで，すべてログに表示する。（まだ送信しない）
* 現在から10分前の送信日時でフォームから送信を行い，doPeriodicallyを手動実行せよ。
* 現在から10分後の日時でフォームから送信を行い，doPeriodicallyを手動実行せよ。
* 12分後位にdoPeriodicallyを再度手動実行せよ。
* 上3つの実行結果の部分のログ（フォーム送信を含む）と，プログラムを提出せよ。kadai9.txtとして提出せよ。ログは，log9.txtとせよ。
* ログは適宜改行やキーワードを挿入し，見やすくわかりやすくなるよう心がけること。理解しづらい場合は再提出してもらう場合がある。



```java:参考
function doPeriodically(){//時間ベースで定期的に実行される関数
  log = new Doc(logID);
  var folder = DriveApp.getFolderById("****************");//mailフォルダのIDは事前に調べておく
  var files = folder.getFiles();// mailフォルダ内を検索，ファイルの一覧を取得

  while(files.hasNext()){// ファイルが一個でもあれば
    var file = files.next();//ファイルを取得
    var fileName = file.getName();//ファイル名をゲット
    log.print(fileName + "\n");
    var fileID = file.getId();//ファイルIDをゲット



  }


}
```

* `  var todayNow = getDateAndTime(0);//現在日時をゲット`としておけば，次のように文字列比較でも日時の比較ができるので利用するとよい。もちろん日付を1000000倍して時間を足して比較してもよい。

```java:新旧の比較が文字列でできてしまう！
    if(fileName<=todayNow) log.print("送信する\n");
    else                   log.print("送信しない\n");
```

__注意：__
上のサンプルプログラムで，日付と時間のうち，時間がずれる場合がある。フォーム入力でデータが追加入力されていくスプレッドシートで，ファイル，設定，をクリックし，

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146927/a7cf6fff-c84e-cccf-a7ff-0df746f81f2d.png)

言語と地域が，日本，Tokyo，になっているか確認せよ。（タイムゾーンが，Delhi，になっている場合がある。どうもGASを開発している拠点のタイムゾーンになっている可能性が高い。2022年は問題なかったので，Googleの開発においてバグが発生したものと思われる）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146927/63f5a4f4-dde1-dd8f-7d16-806b7fa2eeeb.png)








__＜課題10＞__
　__次の課題を行え。__
◎時間指定メール送信システムを以下の指示に従い完成させよ。
◎doPeriodically()のトリガーは１分ごとに設定せよ。
◎フォルダ内のファイル削除は，file.setTrashed(true);を使う。使い方は検索して調べよ。
◎logやmailフォルダの中身を確認し，正常動作することを確認せよ。
◎完成したら，現在時刻より10分以上後に，matsu@tokyo-ct.ac.jpにメールが届くように設定し，その時刻に先生のところに行って，確認してもらうこと。
◎メールの内容には， __フォームにメール内容を入力した時間__ と，__送信設定日時（実際に送信される，日付と時間）__ を必ず書いておくこと。（スプレッドシートから読み取ってメール本文に付け足す）
◎課題を提出したら，<font color="Red">__サーバに余計な負荷をかけないため，doPeriodically()のトリガーを削除__</font>せよ。（今後も使用する場合はそのときにトリガー設定し直せ）
◎完成した，コード.gsをkadai10.txtとして提出せよ。
◎logをlog10.txtとして提出せよ。<font color="Red">__動作がわかるようにlogを構成すること。__</font>この例ではフォームからメールを入力後，mailフォルダに二つのファイルが存在し，片方は，12:25に送信され，まだ１通残っていることがわかる。

```text:参考
20170522_122243 スクリプト開始
他のスクリプト実行要求をロック完了，最大60秒
最後の行12 最後の列8
20170522
122500
最後に入力されたデータを新規Spreadsheetにコピー終了，ファイル名を送信日時に変更
20170522_122247 ロック解除，次のスクリプト要求を受け付け開始
現在日時は20170522_122313
20170522_122500  送信しない
20170522_124000  送信しない
現在日時は20170522_122413
20170522_122500  送信しない
20170522_124000  送信しない
現在日時は20170522_122513
20170522_122500  送信する
データ取得完了
メール送信完了
ファイル削除完了
20170522_124000  送信しない
現在日時は20170522_122613
20170522_124000  送信しない
現在日時は20170522_122713
20170522_124000  送信しない

```

__＜課題11＞（提出は義務付けない）__
　__次の課題を行え。__
◎[このサイト](https://keisan.casio.jp/exec/system/1161228729)と[このサイト](https://keisan.casio.jp/exec/system/1161228732)を参照し，カウプ指数値，ローレル指数値，ボディマス指数を理解せよ。
◎メールアドレス，氏名，年齢（ラジオボタンかリストで選択），身長，体重を入力するフォームを作成せよ。
◎年齢に応じて指数を選択し，指数値と判定を返すクラスを作れ。
◎メソッドには，getIndexName(適当な引数);　getIndexValue(適当な引数);で指数名と指数値を得られるものとする。なお，できる学生は，getIndex.Name(適当な引数);　getIndex.Value(適当な引数);で指数名と指数値を得られるようにせよ。
◎フォーム入力後，入力内容，指数名，指数値をメールで返すようにプログラムせよ。
◎提出物は，ソースリストと動作確認のためのフォームのURLとする。URLはソースの一番上にリマーク分としてかいておけばよい。