2019年9月15日日曜日

フォトスポット:横浜万国橋

横浜みなとみらい地区の夜景撮影ポイントとして外せないのは万国橋から谷戸マークタワーやコスモクロックをPixel3の夜景モードで撮影してみた。
同じ構図で何枚か撮影し、Googleフォトでアニメーション作成してみた。多少構図がずれていてもうまく調整してくれている模様。
書くまでもなくわかると思いますが、撮影ポイントは以下です。

2019年9月8日日曜日

Googleのバックアップと同期の設定画面

Googleのバックアップと同期の設定画面は文章だけで説明するのが面倒なので設定画面のキャプチャ画像を張ってみた。

1.タスクトレイの通知領域に表示されるGoogleのバックアップと同期アイコン


1-1.待機状態のGoogleのバックアップと同期アイコン
1-2.同期実施状態のGoogleのバックアップと同期アイコン

2.Googleのバックアップと同期アイコンクリック時のポップアップメニュー


2-1.Googleドライブアイコンのポップアップメニュー

3.Googleのバックアップと同期設定画面

3-1.マイ パソコン



3-1-1.設定画面:マイ パソコン
バックアップしたいフォルダにチェックを入れます
バックアップしたいフォルダを①のリストに追加します
バックアップ対象のファイルを選択します。
「すべてのファイル」か「写真と動画だけ」か選びます。
写真と動画のアップロードサイズを選択します。
「高画質」か「元の画質」か選べます。
「高画質」は画質の変化を伴います。
ファイル削除時の動作を選択します。Googleフォトには影響を与えません。
常に両方のコピーを削除する: パソコンまたは drive.google.com からアイテムを削除すると、他の場所からも削除されます。
両方のコピーを削除しない: パソコンからアイテムを削除しても、drive.google.com からは削除されません。
両方のコピーを削除する前に確認メッセージを表示する: パソコンからアイテムを削除するときに、他の場所からも削除するかどうかを確認するメッセージが表示されます。
写真をGoogleフォトにアップロードするときにチェックを入れます
カメラなどのデータを直接アップロードしたいときに設定します。

3-2.Googleドライブ


3-2.設定画面:Googleドライブ

3-3.設定


3-2.設定画面:設定

2019年8月15日木曜日

デジカメの画像をGoogleフォトにアップロードする方法(iPhone/iPad)

デジカメで撮影した写真をGoogleフォトにアップロードする方法としては一度パソコンに取り込んでから「バックアップと同期」を用いてバックアップする方法がある。
しかし、「バックアップと同期」を用いてGoogleフォトにアップロードするとドライブ側にもアップロードされてしまいます。また、写真をバックアップするためにパソコンを起動する必要もあります、めったにパソコンを使用しない私としては非常に煩わしい。

そこで、おススメの方法はLightning SDカードカメラリーダーを使用して、デジカメの写真をiPhoneやiPadのカメラロールに取り込み、「Googleフォト」アプリでGoogleフォトにバックアップする方法です。カメラロールへの取り込みは迷うこともなく、簡単に行えるので特に方法は説明しません。旅行中にデジカメの写真を手軽にバックアップできることも魅力の一つです。
名前の通り、Lightningコネクタに接続してSDカードに保存されているデジカメのデータをカメラロールに読み込むためのものだが、アップル純正という安心感と写真の管理が一気に楽になることを考えると¥2,800 (税別)は非常にお買い得だとおもう。(¥3,200で購入したのにいつの間にか値下げされている、、、が満足している。)
カメラデータ以外読み込めないし、リーダーというだけあって書き込むこともできない、サードパーティ製のSDカードアダプタはもう少し高機能かもしれないので、そういったものにも興味がある人は調べてから購入するとよい。

我が家の目的としてはSDカードリーダーが良かったのですが、Lightning USBカメラアダプタLightning USB 3カメラアダプタUSB-C SDカードリーダーなどもあるので用途に合わせて選んでください。

「互換性」の項目にご自身の端末が登録されていることも忘れずにチェックしてください。

2019年1月19日土曜日

Google Apps Script で指定フォルダ以下のファイルを処理する方法(3)

Google Driveで指定したフォルダ以下に配置されているすべてのファイルに対して処理を行うGoogle Apps Script(GAS)を考えてみた。普通に考えるとフォルダ内のファイルとフォルダを処理する関数を再帰的に呼び出すのだが、、、GASには6分以上経過すると強制的にスクリプトが終了してしまう制限がある。

この6分の壁対策として時間トリガを設定して中断した処理を再開する必要がある。しかしその再開のためにはどこまで処理したかを保存しなけれがならない。一般的には中断するときにFileIteratorクラスのgetContinuationToken()を使用してトークンを取得しそれをスクリプトプロパティなどに保存して、時間トリガで起動した際にはDriveAppクラスのcontinueFileIterator(continuationToken)でFileIteratorを復元する。

しかし、再帰的に関数を呼び出している処理を中断するのは少しコツがいる。

今回は単純な方法を紹介する。
  1. ドライブで管理しているすべてのFileIteratorを所得する
  2. FileIteratorですべてのファイルをスキャンする
  3. ファイルが指定したフォルダ配下かどうかをチェックする
  4. 5分経過した場合にはFileIteratorをプロパティに保存する
ただし、この方法には欠点がある。すべてのファイルで、親フォルダをスキャンするので処理が重く、FileIteratorは1週間という有効期限があるため、ドライブ内のすべての処理が1Week以内で終わるようにトリガタイミングと処理を調整しなければならない。

そのうち6分の壁対策版の再帰処理コードも載せると思う。

function start()
{
  // プロパティとトリガをすべて削除
  PropertiesService.getScriptProperties().deleteAllProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }

  // トリガを設定する。(10分間隔)
  trigger = ScriptApp.newTrigger("triggerFunction")
  .timeBased()
  .everyMinutes(10)
  .create();
  triggerFunction();
}

function triggerFunction() {
  var StartTime   = new Date();
  var TargetFolder = "1zXo...処理対象のフォルダID....xEfEI";

  var ContinuationToken = PropertiesService.getScriptProperties().getProperty("ContinuationToken");
  if( ContinuationToken != null )
  {
    var files = DriveApp.continueFileIterator(ContinuationToken);
  }
  else
  {
    var files = DriveApp.getFiles();
  }

  while(files.hasNext()) {
    var CurrentTime = new Date();
    // トリガを設定する。(5分間実行していたら止める。)
    if( (CurrentTime - StartTime) > 5*60*1000 ){
      PropertiesService.getScriptProperties().setProperty("ContinuationToken",files.getContinuationToken());
      return;
    }
    var file = files.next();
    if( checkSubordinate( file, TargetFolder ) )
    {
      ////////////////////////////////////
      // ファイルごとの処理をここから記載する。

      // ★★ ここにfileに対する処理を記載する ★★

      // ファイルごとの処理をここまでに記載する。
      ////////////////////////////////////
    }
  }
  
  PropertiesService.getScriptProperties().deleteAllProperties();
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
}

// targetが指定フォルダ(folder ID)の配下のフォルダかチェックする。
function checkSubordinate(target,folderID)
{
  var parents = target.getParents();
  while( parents.hasNext() )
  {
    var parent = parents.next();
    var id = parent.getId();
    if( id === folderID ){
      return true;
    }
    if( checkSubordinate(parent,folderID) )
    {
      return true;
    }
  }
  return false;
}

2019年1月13日日曜日

Google Apps Script小ネタ集(1)


Google Apps Scriptでたまに欲しくなる小ネタをまとめた。

  • フォルダの作成・取得
  • 多階層フォルダの作成・取得
  • テキストファイル保存
  • ファイルへのデータ保存
  • 指定期間のメールを処理する


フォルダの作成・取得

始めにフォルダ作成のダメな例ですが、普通にcreateFolderを呼び出すと、既に同じ名前のフォルダが存在していても構わず同じ名前のフォルダを生成する。そのためトリガを設定して何度も実行されるスクリプトでこれを行うと同じ名前のフォルダがどんどん増えていき使い物にならない。
// ダメな例
// すでに存在していてもフォルダを作成する。
// 何度も実行すると、毎回同じ名前のフォルダが生成される。
DriveApp.getRootFolder().createFolder( 'HogeHoge' );
対策としては既にフォルダが存在していたらそのフォルダを使用し、存在していない場合には新規に作成することにする。ちなみにファイルやフォルダが存在しているかどうかはgetFoldersByName( name ) 関数を使用する。Foldersと複数形となっていることからもわかるように、同じ名前のフォルダが複数あった場合には複数のフォルダが見つかる。その場合どのフォルダを採用するか悩んでも仕方がないので、最初に見つかったものを採用する。
var folderName = 'HogeHoge';
var root = DriveApp.getRootFolder();    
var folders = folder.getFoldersByName( folderName );
if( folders.hasNext() ) {
  // フォルダが見つかった場合は最初のフォルダを採用
  folder = childs.next();
} else {
  // フォルダが見つからなかった場合はフォルダを作成
  folder = folder.createFolder( folderName );
}
これを関数化しておく。
/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string} name 取得・生成するフォルダ名
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
 */
function getFolder( parent, name, noCreate ) {
  if ( parent == null ){
    parent = DriveApp.getRootFolder();
  }
  if ( noCreate !== true ){
    noCreate = false;
  }
  var folders = parent.getFoldersByName(name);
  if( folders.hasNext() ) {
    // フォルダが見つかった場合は最初のフォルダを採用
    var folder = folders.next();
  } else {
    if( noCreate ){
      // noCreate指定の場合はundefinedを返す。
      var folder = undefined;
    }else{
      // フォルダが見つからなかった場合はフォルダを作成
      var folder = parent.createFolder(name);
    }
  }
  return folder;
}


多階層フォルダの作成・取得

一階層のフォルダを生成するだけであれば先ほどの処理でよいが、写真を整理する場合などは年フォルダの下に月フォルダを生成したい場合がある。単純に処理を繰り返すだけだが、毎回コードを書くのが面倒なので関数化しておく。
/**
 *  指定されたフォルダ取得する、フォルダがないときは新規に作成する。
 *
 *  @param {Folder} parent 基準フォルダ、null指定時はルートフォルダ
 *  @param {string|Array of strings} name 取得・生成するフォルダ名
 *      string 指定時はnameを取得。生成する
 *      Array of strings 指定時は多階層フォルダを一括作成
 *  @param {bool} noCreate 作成は行わず既存のフォルダを取得するときにtrue
 *      省略可能、省略時はfalseとして動作
 *  @return {Folder|undefined} 取得・生成したフォルダ
*/
function getFolder( parent, name, noCreate ) {
  // 親フォルダが指定されなかった場合はルートフォルダを取得する。
  if ( parent == null ){
    var folder = DriveApp.getRootFolder();
  }else{
    var folder = parent;
  }
  // フォルダ名に配列が指定されていない場合は配列にする。
  if( !Array.isArray( name ) ){
    name = [name];
  }
  // noCreateが指定されていない場合はfalseにする。
  if ( noCreate !== true ){
    noCreate = false;
  }

  while( name.length > 0 ){
    var newFolder = name.shift();
    var childs = folder.getFoldersByName( newFolder );
    if( childs.hasNext() ){
      // フォルダが見つかった場合は最初のフォルダを採用
      folder = childs.next();
    } else {
      if( noCreate ){
        // noCreateの場合はnullを返す
        return undefined;
      }else{
        // フォルダが見つからなかった場合はフォルダを作成
        folder = folder.createFolder( newFolder );
      }
    }
  }
  return folder;
}
使い方は以下。
function myFunction() {
  // ルートフォルダにHoge1フォルダを生成
  var f1 = getFolder( null,'Hoge1' );  

  // ルートフォルダにあるHoge2フォルダを取得
  var f2 = getFolder( null,'Hoge2', true );
  if( f2 == undefined ){
    Logger.log('Did Not create folder.' )
  }

  // Hoge1フォルダの下に多階層フォルダを生成
  var v3 = getFolder( f1,['Hoge3','Hoge3-1','Hoge3-2'] );
  // 以下のフォルダ構成の'Hoge3-2'フォルダを取得
  // \\Hoge1\Hoge3\Hoge3-1\Hoge3-2
}


テキストファイルを保存

テキストファイルへの保存と読み込みはDrive Serviceのリファレンスに従いうだけではあるが、毎回悩むので記録しておく。
// 新規ファイルの生成と追記
var folder = DriveApp.getRootFolder();
var file = folder.createFile( 'filename.txt', 'Hello, world!!' );
var text = file.getBlob().getDataAsString();
file.setContent(text + '\nNew line!!' );
// 既存ファイルの読み込みと追記
var files = folder.getFilesByName( 'filename.txt' );
if( files.hasNext() ){
  var file = files[0];
  var text = file.getBlob().getDataAsString();
  file.setContent(text + '\nNew line!!' );
}


ファイルへのデータ保存

テキストの読み書きができているので、クラスを保存したい場合などはJSON化すればよい。
// 保存するオブジェクトの配列を定義
var Users = [new User(123,'Taro'),new User(124,'Jiro')];

// オブジェクトをJSONに変換してファイルに保存
var json = JSON.stringify(Users);
var file = folder.createFile( 'filename.txt', json );

// 保存したJSONを読み込んでオブジェクトを復元
var Users2 = JSON.parse(file.getBlob().getDataAsString());
保存されたファイル(filename.txt)は以下。
[{"id":123,"name":"Taro"},{"id":124,"name":"Jiro"}]


指定期間のメールを処理する

GMAILの検索はGmailApp.search(query,start,max)を使用する。しかしこの関数の戻り値はGmailThread[]となっており非常に厄介。なぜかというと、期間を指定検索をすると期間内のメールも含むスレッドが検索されるのだが、そのスレッドには期間外のメールも含まれていることがある。
今回はその期間外のメールを除外したメールのID一覧を取得する関数の紹介。といっても、期間指定して検索した結果をループで回して期間内のメールだけを抽出している。特に工夫していないが、いざ欲しいときにコーディングしようと思うと時間を取られるので残しておく。
/**
 *  メールの検索結果から指定した期間のメールのIDリストを取得する。
 *
 *  @param {string} searchString 期間指定以外の検索条件
 *  @param {Date} start 検索期間の開始
 *  @param {Date} end 検索期間の終了
 *  @return {string[]} メールIDリスト
 */
function getMailIdList( searchString, start, end )
{
  var after = 'after:' + Math.floor( start.getTime()/1000 ).toString();
  var before = 'before:' + Math.floor( end.getTime()/1000 ).toString();
  const search = after + ' ' + before + ' ' + searchString;

  var mailIdList = [];
  var indexStart = 0;
  while( true )
  {
    var myThreads = GmailApp.search(search, indexStart, 500);
    if( myThreads.length==0)
    {
      return mailIdList;
    }
    indexStart+=myThreads.length;
    
    var myMsgs = GmailApp.getMessagesForThreads(myThreads);
    for(var thread = 0;thread < myMsgs.length;thread++){
      for(var msg = 0;msg < myMsgs[thread].length;msg++){
        var reciveDate = myMsgs[thread][msg].getDate();
        if( start < reciveDate && end > reciveDate )
        {
          mailIdList.unshift(myMsgs[thread][msg].getId());
        }
      }
    } 
  }
}
ちなみに使い方は以下を想定している。
MailList = getMailIdList('SearchString',new Date('2018/10/01 0:00:00'),new Date('2018/10/01 13:00:00'));
while( MailList.length > 0 )
{
  var mail = GmailApp.getMessageById(JobList.shift());
  mail.xxxXxxxx(); // メールに対する処理を実施
}

2019年1月2日水曜日

Google Apps Script でプロパティをファイルに保存する

GoogleAppsScriptは便利だがいろいろと困ることがある、その代表格は6分制限。スクリプトの実行時間が6分を超えると途中で処理が終了してしまいます。 その対応として実行経過を保存し時間駆動型のトリガーを設定して、細切れに処理をする必要がある。これについてはGoogleで検索すれば山のように情報が出てくるので割愛する。
今回その途中経過を保存する方法について書いてみる。 一般的にはProperties ServicesetProperty(key, value)などを用いると非常に楽である。しかし、9217文字以上の文字列を扱おうと思うとエラーが発生してしまう。

var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty(
  'key_string',  // keyにもサイズ制限があるかもしれないが未検証
  'ここに文字数が9217以上の文字列を指定、XXXXXXXXXX');
}

上の例では以下のようにエラーではじかれる。
引数が大きすぎます:value(行 nn,ファイル「コード」)
そこまで大きい値を扱わない場合には問題ないが、DriveAPIで複雑な検索式でsearchFilesを行った場合のContinuationTokenがこの上限を大幅に超えることがある。9216文字以下になるように分割して複数のプロパティに保存することで対処可能ではあるが、今回はプロパティをファイルに保存する方法を考えてみたので忘れないようにメモ。使い方はそのうち別記事で記載します。

初回起動時にプロパティファイルを生成し、そのファイルIDをProperties Serviceで保存しておく。ファイルIDが変わらない限りはプロパティファイルの名前が変更されても参照が継続する。使い勝手が良いのか、悪いのかはよくわからない。
// 使い方はfunction examplePersistentProperty()を参照

// PersistentProperty()
// 概要:クラスのコンストラクタ
// 引数:filename  プロパティ保存する初期ファイル名
// 戻り値:インスタンス
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
// 備考:一度生成したファイルを参照し続ける。ファイル名を変えても参照し続ける。
PersistentProperty = function(filename){
  this.file = null;
  var id =  PropertiesService.getUserProperties().getProperty("PropertiesFileID");
  try{
    if( id!=null )
    {
      // ファイル取得を試みる
      this.file = DriveApp.getFileById(id);
      if( this.file.isTrashed() )
      {
      // ユーザープロパティから取得したファイルがゴミ箱にある場合には廃棄する
        this.file=null;;
      }
    }
  }
  catch(e)
  {
    // ファイル取得に失敗した場合は
    this.file=null;
  }
  // プロパティファイルがない場合には新規に作成する。
  if( this.file==null )
  {
    var name = filename;
    this.file =  DriveApp.createFile(name,"{}",MimeType.PLAIN_TEXT);
    var fileId = this.file.getId();
    PropertiesService.getUserProperties().setProperty("PropertiesFileID",fileId);
  }

  // ファイルからテキスト読み込み
  var text =  this.file.getBlob().getDataAsString();
};


// deleteAllProperties()
// 概要:保存されているプロパティをすべて消去
// 引数:なし
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteAllProperties();
// 備考:
PersistentProperty.prototype.deleteAllProperties = function(){
  this.file.setContent(JSON.stringify({}));
};

// deleteProperty(key)
// 概要:指定したプロパティを消去
// 引数:key  削除するプロパティを指定するキー
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.deleteProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.deleteProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  try{
    // 指定されたプロパティ削除
    delete Properties[key];
  }
  finally
  {
    // 最終的にオブジェクトをファイルに保存
    this.file.setContent(JSON.stringify(Properties));
  }  
};

// getKeys()
// 概要:保存しているプロパティのキーの一覧を取得
// 引数:なし
// 戻り値:キーの一覧(string[])
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var keys = pp.getKeys()
// 備考:
PersistentProperty.prototype.getKeys = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  var keys = [];
  for (key in Properties) {
    keys.push(key);
  }
  return keys;
};

// getProperties()
// 概要:保存しているプロパティリストを取得
// 引数:なし
// 戻り値:保存しているプロパティリスト(連想配列)
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var properties = pp.getProperties();
//   var value = properties['PropaertyKeyString'];
// 備考:
PersistentProperty.prototype.getProperties = function(){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);
  return Properties;
};

// getProperty(key)
// 概要:指定したプロパティを取得
// 引数:key  取得するプロパティを指定するキー
// 戻り値:プロパティの値
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   var value = pp.getProperty('PropaertyKeyString');
// 備考:
PersistentProperty.prototype.getProperty = function(key){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( key in Properties ){
    return Properties[key];
  }else{
    return null;
  }
};

// setProperties(properties,deleteAllOthers)
// 概要:プロパティリストを設定
// 引数:properties  設定するプロパティリスト(連想配列)
//   :deleteAllOthers  trueを指定すると他のプロパティをクリア、書略時はfalseとして動作する
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperties({ "key1":"value1","key2":"value2"},true); // 他のプロパティは削除
//   pp.setProperties({ "key3":"value3","key4":"value4"}); // プロパティを追加
// 備考:deleteAllOthersがfalseの場合はプロパティリストの追加
PersistentProperty.prototype.setProperties = function(properties,deleteAllOthers){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  if( deleteAllOthers ){
    Properties = properties;
  }else{
    for( var key in properties ){
      Properties[key] = properties[key];
    }
  }
  var json = JSON.stringify(Properties);
  this.file.setContent(json);
};

// setProperty(key,value)
// 概要:プロパティを設定
// 引数:key  設定するプロパティのキー
//   :value  設定するプロパティの値
// 戻り値:なし
// 使い方:
//   var pp = new PersistentProperty('PersistentPropertyTest');
//   pp.setProperty( "key1", "value1" );
// 備考:
PersistentProperty.prototype.setProperty = function(key,value){
  var text =  this.file.getBlob().getDataAsString();
  var Properties =  JSON.parse(text);

  Properties[key] = value;
  this.file.setContent(JSON.stringify(Properties));
};

//////////////////////////////////////////////////////////////////////////////
// これ以降は使い方サンプル及びDEBUG用
//////////////////////////////////////////////////////////////////////////////
function examplePersistentProperty()
{
  // ファイル名を決める
  var pp = new PersistentProperty('PersistentPropertyTest');
  
  // プロパティのセット
  pp.setProperty( "test1","test1 value" );

  // プロパティを複数追加
  pp.setProperties(
    { "test2":"test2 value","test3":"test3 value"});

  // プロパティを複数追加
  pp.setProperties(
    { "test4":"test4 value","test5":"test5 value"},false );

  // 一つずつ取得してもよい
  var test1 = pp.getProperty("test1");
  
  // 一気に取得してもよい
  var properties = pp.getProperties();
  var test2 = properties["test2"];
 
  // プロパティを一部削除
  pp.deleteProperty("test4");
  
  // プロパティを一部削除や追加し一気に更新
  delete properties["test5"];
  properties["test6"] = "New Property";
  pp.setProperties(properties,true);
  
  // 全プロパティを列挙
  var keys = pp.getKeys();
  for( var i=0; i<keys.length; i++ ){
    var key = keys[i]
    var value = pp.getProperty(key);
  }

  // 全プロパティを削除
  pp.deleteAllProperties();
}

GMAILに受信したメールをGoogle Apps Scriptで処理する

GMailに受信したメールを処理するスクリプトを作成した。
検索を行いその結果に対して処理を行うが、一度処理したメールは再度処理しないようにしなければならない。処理をしたメールにたいし、ラベルをつけたり、既読化したり、スターをつけたりすることで対処しようとしているページが多い。ラベルや既読化はスレッド単位で検索されてしまうので問題が発生するし、既読化やスターをつける方法は複数のスクリプトで処理したいメールがある場合に対応できない。

今回はメール受信日時で処理済みかどうかを判断する方法を採用した。時間駆動トリガで定期的に実行することを想定しているが、トリガを仕掛ける処理は組み込んでいないので、手動でトリガ指定する必要がある。処理量が多いと一回のスクリプト実行時間(6分)では処理しきれないこともあるため中断することも考慮に入れている。
目的に合わせて青字の部分をカスタマイズして使用する。


/// トリガ設定は行っていないので、手動で指定する必要があります。

function Main() {
  var ScriptStartTime = new Date();
//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズはここから
////////////////////////////////////////
  // メールを検索する条件(beforとafterは指定しない)
  var SearchString = 'in:all -in:spam -in:trash has:attachment';
    
  // 処理対象期間
  var SearchStartDate = new Date('2018/10/00 0:00:00');
  var SearchEndDate = new Date(ScriptStartTime);

  // 一回の検索で検索する期間5分で処理できる量を指定するのが一番良い。
  var IncrementHours = 24;

  // メールに対する処理(例:メール受信日時フォルダを生成し、その中に添付ファイルを保存する)
  var Action = function(mail)
  {
    var DriveFolderName = "GmailAttachments";  
    var reciveDate = mail.getDate();
    var folderID = getDriveFolder([DriveFolderName,reciveDate.getFullYear(),reciveDate.getMonth()+1,reciveDate.getDate()]);
    var attachments = mail.getAttachments();
    for (var n = 0; n < attachments.length; n++)
    {
      folderID.createFile(attachments[n]);
    }
  }  
////////////////////////////////////////
// カスタマイズはここまで
//////////////////////////////////////////////////////////////////////////////////////

  var properties = PropertiesService.getScriptProperties().getProperties();
  
  // 検索開始日時をプロパティから取得する
  if( 'SearchStartDate' in properties ){
    SearchStartDate = new Date(properties['SearchStartDate']);
  }

  // 実施すべきジョブ(MailIDの配列)をプロパティから取得する。
  var JobList = [];
  if( 'JobList' in properties ){
    JobList = JSON.parse(properties['JobList']);
  }

  for( var start = SearchStartDate; start < SearchEndDate; )
  {
    // メール検索期間の設定
    var end = new Date(start);
    end.setHours(end.getHours() + IncrementHours);
    if( end > SearchEndDate ){
      end = new Date(SearchEndDate);
    }
    
    // JobListの生成(処理すべきメールのIDリスト)
    if( Object.keys(JobList).length == 0 )
    {
      JobList = getMailIdList(SearchString,start,end);
    }
    
    // JobListに従い処理実行
    while( JobList.length > 0 )
    {
      // スクリプト開始から5分経過している場合は実行経過をプロパティに保存して処理を中断する。
      var CurrentTime = new Date();
      if( (CurrentTime - ScriptStartTime) > 5*60*1000 )
      {
        properties['SearchStartDate'] = start;
        properties['JobList'] = JSON.stringify(JobList);
        PropertiesService.getScriptProperties().setProperties( properties );
        return;
      }
      
      Action(GmailApp.getMessageById(JobList.shift()));   // 処理実行関数呼び出し
    }
    
    // 検索期間を更新
    start = end;
  }
  properties['SearchStartDate'] = start;
  properties['JobList'] = JSON.stringify(JobList);
  PropertiesService.getScriptProperties().setProperties( properties );
}

// 指定期間で検索を行い、指定期間に受信したメールのIDを配列で取得する
function getMailIdList( searchString, start, end )
{
  var after = 'after:' + Math.floor( start.getTime()/1000 ).toString();
  var before = 'before:' + Math.floor( end.getTime()/1000 ).toString();
  const search = after + ' ' + before + ' ' + searchString;

  var mailIdList = [];
  var indexStart = 0;
  while( true )
  {
    var myThreads = GmailApp.search(search, indexStart, 500);
    if( myThreads.length==0)
    {
      return mailIdList;
    }
    indexStart+=myThreads.length;
    
    var myMsgs = GmailApp.getMessagesForThreads(myThreads);
    for(var thread = 0;thread < myMsgs.length;thread++){
      for(var msg = 0;msg < myMsgs[thread].length;msg++){
        var reciveDate = myMsgs[thread][msg].getDate();
        if( start < reciveDate && end > reciveDate )
        {
          mailIdList.unshift(myMsgs[thread][msg].getId());
        }
      }
    } 
  }
}

//////////////////////////////////////////////////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここから
////////////////////////////////////////
// 名前を指定したフォルダ生成する、すでに存在する場合はそのフォルダを取得する
// 階層構造を一気に作成できます。
// 作成したいフォルダ:(RootFolder)/folderA/folderB/folderC
// 呼び出し方:getDriveFolder(["folerA","folderB","folderC"]);
function getDriveFolder( folderHierarchy, folder ) {
  if ( folder === undefined ) folder = DriveApp.getRootFolder();    
  for(var h = 0;h < folderHierarchy.length; h++ ){
    var childs = folder.getFoldersByName(folderHierarchy[h]);
    if( childs.hasNext() ) {
      // 指定された名前のフォルダがあるときはそのフォルダを使用
      folder = childs.next();
    } else {
      // 指定された名前のフォルダが見つからなかったときはフォルダを生成
      folder = folder.createFolder( folderHierarchy[h] );
    }
  }
  return folder;
}
////////////////////////////////////////
// カスタマイズ部分から呼び出している関数はここまで
//////////////////////////////////////////////////////////////////////////////////////

メールから必要な部分を抜き出してスプレッドシートに張り付ける場合のAction関数は以下のような感じで実現した。

  // メールに対する処理
  var Action = function(mail)
  {
    // 処理サンプル:以下のメールから必要な部分をシートに張り付ける。
      //    氏 名 様
      //    
      //    いつも ANAプリペシルバーマイルコース をご利用いただき、
      //    ありがとうございます。
      //    氏 名 様のカードのご利用がありましたのでご連絡します。
      //    
      //     【ご利用明細の承認番号】
      //      1234567
      //    
      //     【ご利用カード番号】
      //      ****-****-****-0000
      //      ※セキュリティ保護のため、下4桁のみ表示しています。
      //    
      //     【ご利用日時(日本時間)】
      //      2018/12/29 17:20
      //    
      //     【ご利用先】
      //      セブン-イレブン
      //    
      //     【ご利用金額】
      //      1,234 円
      //    
      //     【バリュー残高】
      //      123,456 円
      //    
      //    ご利用先は、加盟店からの売上情報到着前の情報となり、実際のご利用先と名称が異なる場合があります。
      //    (売上情報到着後の名称については、マイページにログインのうえ「ご利用明細の確認」メニューよりご確認ください。)
      //    なお、携帯電話料金などの月額料金のお支払いに登録され、
      //    9:00PM以降にバリュー残高からの加算・減算が発生した場合は、翌8:00AM以降のメール送信となります。
      //    
      //    ご不明点などありましたら、JCBプリペイドカードデスクにお問い合わせください。    
    
    
    var sheet = SpreadsheetApp.getActiveSheet();
    var valTitle = [["【ご利用日時(日本時間)】","【ご利用先】","【ご利用金額】","【バリュー残高】","【ご利用明細の承認番号】","【ご利用カード番号】"]];
    var i = sheet.getLastRow();
  
    if(i==0){
      sheet.getRange(1, 1, 1, valTitle[0].length).setValues(valTitle); //シートに貼り付け
      i=1;
    }
    var bodys = mail.getPlainBody().replace(/[\t \r]/g,'').split("\n");
    var valMsgs=[];
    valMsgs[0]=[];
    for(var l = 0;l < valTitle[0].length; l++ ){
      valMsgs[0][l] = bodys[bodys.indexOf(valTitle[0][l])+1];
    }
    sheet.getRange(i+1, 1, 1,valTitle[0].length).setValues(valMsgs); //シートに貼り付け
  }