2014/07/13

Gmail と GoogleAppScriptで・・・

・・・メールの返信(ブログの投稿)と Twitterへのつぶやきを タイマー予約出来る様にして診た。


警告 - 20150418 141039)

このスクリプト中、Twitter投稿での認証処理で用いている OAuthConfig が
今年頭から出ていた警告通り、20150420に廃止され 利用出来なくなります。

それに伴い、このスクリプトでは Twitter投稿部分が機能しなくなります。

同ログ中でもお世話になった きじとらさんのトコで、該当部分の代替のスクリプトが紹介されています。

おきつねさまのスクリプトのほうでも、ソチラへ差し替えを行う予定にしていますが、
単にTweetするだけなら別の認証方法もあり、ドチラを用いるべきか 検討しているトコロだったりします。


・・・まぁ このログが雑多に羅列してしまって読み辛いので、代替処理が完成したら新たにログを投稿する予定。
後述する書式の日時文字列を 本来の件名文字列より前につけた件名を持つプレーンテキストの短文メールに 返信先を指定した状態で 以下スクリプトを仕込んだ 該当Googleアカウントのメールアドレスへ送信しておくだけで予約が可能になる。 おきつねさま環境での 実際の運用は、そうしたメールを 自動作成して送信する様に構成された 別のローカル動作なVBScriptとの併用を 前提としているのだが、メールを送る環境さえあれば、件名を適宜入力し 対象アドレスへ送信するだけで 同様に利用出来るようになっている。
 !!!!! Update 20140723 091613 !!!!! 

送信済みメールにもスターが付き、次回以降の処理ループ対象となってしまっていた。
対策として スターを外す処理を追記。
コレを実現するには、当然ながら Googleのアカウントが必要になる。 スクリプトに関しては、この ▼ きじとらさんトコのソースをベースに・・・
Gmailで指定日時に送る ってのをGoogle Apps Scriptで書いた - きじとら [ 20130915 ]
対象オブジェクトを [下書き] から [受信トレイ] ないし [スター付きメール] に、 日付文字列書式を "{yyyy/mm/dd hh:nn} " から "{yyyy/mm/dd hh:nn:ss} " に 送り先は メールにセットされている[送信先アドレス] から [返信先アドレス] へ それぞれ合わせて変更してある。 と云うのも、Gmailの [下書き] は ブラウザで操作して用意する必要があるので おきつねさまの用途的に使い悪く、 対象を [受信トレイ] か [スター付き受信メール] に変更する必要があり、且つ 日付文字列書式に関しては おきつね環境での VBScript と Windows の 標準ロケールに合わせカタチ・・・
コレなら 予約メールの件名書式を  
{yyyy/mm/dd hh:nn:ss} メール件名文字列
・・・と してあれば、
自分のメールアカウントで そのメールアカウントのアドレス宛に、メールの返信先として Bloggerなどでサポートされている [ブログ投稿メールアドレス] などを セットした状態で送信すれば、指定日時に ブログを投稿してくれる と云う運用。
VBScriptでのメール投稿であれば 送信の際に[返信先(ReplyTo)]アドレスをセット出来るが、
MS-OutlookなどのMAPIクライアントでは、面倒だがアカウント設定画面でしか指定出来ない。

対処としては、Outlookなら 同じGmailアカウントで もう1つ別名を充てたOutlook内での[電子メールアカウント]を追加し、
その[詳細設定]画面 [全般]タグ [その他のユーザー情報]枠 [返信電子メール]項目に、対象となるメールアドレスを設定する。

その準備が出来てれば、以後は メール作成画面で 利用する送信アカウントを選択出来るので、
追加したアカウントで予約メールを送信するだけでいい。
上述 件名文字列は 日時を囲むカッコ記号と 日時文字列とタイトル文字列の間に半角スペースが それぞれ必須である点に注意。
{2014/07/13 21:27:41} メール件名文字列
← 因って、使う際にはこう云うカンジになる。
因みに、手元のWindows機材でVBScriptを用いて日付を扱う場合 その機器のWindowsでのロケール設定に依存するので、 メール送信VBScriptとの併用を行う場合は、以下ソースの対応する必要な行を それぞれ有効/無効にして利用すると良いだろう。 また、スター付きメールを扱うには、Gmailの設定画面で、受信したメールに対してスターを付ける条件を定義した フィルタを設定しておく必要があるのだが、ソレが面倒で、且つ 他の用途に用いていない Gmailアカウントであれば、 受信トレイ全体を検索対象にするオブジェクトのほうを有効にしたほうがいいだろう。 ・・・まぁ実際の用途としては、指定時刻にメールでブログを投稿し、それを IFTTT で 検出させて Twitterの対象アカウントに 適宜Tweetさせたいだけなんだケドねw
 !!!!! Update 20140719 044444 !!!!! 

IFTTTにブログ投稿を監視させてTweetするのでは 遅延のバラつきが酷くて実用に耐えないと判断して廃止。
代わりに このスクリプト自体から直接つぶやくよう変更。

但し、ソレ起因で 仕様と云うか 扱いが大きく変わってしまった・・・

マズ GoogleDrive上で 単体の.gsファイルとしてではなく、
同Drive上の スプレッドシートの マクロ としての スクリプトファイルとなった。
他に機能を追加するには このほうが都合はイイと思うが、ナンか敗北感あるな・・・
 !!!!! Update 20140720 100411 !!!!! 

単体の.gsファイルでも稼動出来ました、スプレッドシートは必要ありません。

自分のTwitterアカに この用途向けのアプリを設定する必要がある。 その登録時に 以下項目を
Callback URL: https://spreadsheets.google.com/macros
Permission: Read & Write
・・・とした上で、他項目も適宜入力して利用準備を完了し、 続いて スクリプトのほうで、上述登録画面にて表示されるトークン文字列 APIKey と APISecret を プロジェクトのプロパティに twitterAPIKey と twitterAPISecret を GUIで 手作業にて追加し、
▼ こう云う処理で GUI使わなくても プロパティにトークンをセットは出来るケド・・・
// PropertySet for Twitter 
function twiProperties() {

  var scriptProperties = PropertiesService.getScriptProperties();

  if(!scriptProperties.getProperty('twitterAPIKey')) {
    scriptProperties.setProperty('twitterAPIKey', 'ココにAPIKey');
  }

  if(!scriptProperties.getProperty('twitterAPISecret')) {
    scriptProperties.setProperty('twitterAPISecret', 'ココにAPISecret');
  }

  return false;

}
それぞれのトークンを 値としてセットする必要がある。
 !!!!! Update 20140720 084645 !!!!! 

因みに、この設定をしなくても Tweet機能が動作しないだけで メール返信は処理されます。
・・・プレーンなままスクリプトに認証情報を置かなくていいのは こうした扱いでは便利だが、少し面倒(´ヘ`;) せめてモノ救いは、Twiアカに紐付いているGoogleアカウントでなくても動作できる点だろうか・・・
GoogleAppScriptのサービス画面内のGUIで 手動で指定するトリガーではなく、 スクリプトの実行で得た 次のメール返信予約時刻を 次回のトリガーとして設定する構造に変更。
 !!!!! Update 20140717 234516 !!!!! 

GoogleAppScriptのタイムアウトは5minだそうな・・・(´ヘ`;)

代わりに スクリプトのトリガー[起動する間隔や時間を指定する機能]のプロパティを
スクリプトで操作する方法が提供されていたので利用してみた。

これでダメなら正直お手上げ・・・


・・・暫定利用向けだから、同一プロジェクト(1つのスプレッドシートファイルや.gsファイル)で
複数の関数をトリガーしてる運用の場合には使えなくなってるので要注意。
 !!!!! Update 20140720 063750 !!!!! 

既存の該当トリガーのみを削除するよう変更。

但し、最初の実行前にだけ、プロジェクトのプロパティとして triggerId を
値なし(未入力の空白のまま)でセットしておかないと エラーで停止すると思うな。
 !!!!! Update 20140720 084645 !!!!! 

初期稼動時に プロパティ'triggerId' が無くても動作するよう補完。

!!!!! Update 20140717 234516 !!!!! まぁ 1hスクリプトを稼動させたままに出来るか? ってのは 概ね期待してなかったが やはりダメだったw 因って、10min毎に動作する指定にあわせて訂正・・・ とりま様子診だな・・・(´ヘ`;) !!!!! Update 20140717 031757 !!!!! GMailのスレッド(オブジェクト)の仕様でアクセス数制限があるようnanoで 1min毎のスクリプト実行を改め、 1時間毎の動作設定に変更し、追加したマネージスクリプトで 必要な時間にのみ 返信スクリプト delaySendReceivedMail() を 処理させるようにしてみた。 仮に 運用に於いて 対象メール数が制限を超える数であった場合は効果がナイのだが、 そうした 突拍子もナイ数のメール返信を扱う予定がナイので コレでヨシとして診た。
さて、前置きはこの辺にして 肝心のソースを・・・
//***** ↓↓↓GlobalDecralations↓↓↓ *********************************************************
var scriptProperties = PropertiesService.getScriptProperties();


//***** ↓↓↓SetupFunctions↓↓↓ *********************************************************

//***************************************************************************************
// SetupPropertys - スクリプトに 必要なプロパティをセットする。
// - Created by LazwardFox - 
//
// プロパティのセット手順の説明が面倒でw
//
// TwiAPIkey と TwiAPISecret に 値をセットしてから実行して下さい。
// この関数は 最初に一度だけ 任意に実行すれば不要となります。
//
// Update 20140723 103638 予約追加が1週間おきと仮定して・・・
// Release 20140722 025606
//***************************************************************************************

function SetupPropertys() {

  var TwiAPIkey = ''; // ←ココに 'TwitterアプリのAPIKey' 
  var TwiAPISecret = ''; // ←ココに 'TwitterアプリのAPISecret' 

  var strFunction = 'delaySendControls'; // セットアップ対象Function。

  if(!scriptProperties.getProperty('twitterAPIKey') && TwiAPIkey) {
    scriptProperties.setProperty('twitterAPIKey', TwiAPIkey);
  }

  if(!scriptProperties.getProperty('twitterAPISecret') && TwiAPISecret) {
    scriptProperties.setProperty('twitterAPISecret', TwiAPISecret);
  }

  // メンテ明け動作用トリガーを新規設定。
  if(!scriptProperties.getProperty('wTriggerIDa')) {
     var w1TrigID = ScriptApp.newTrigger(strFunction).timeBased().atHour(15).nearMinute(0)
                             .onWeekDay(ScriptApp.WeekDay.WEDNESDAY).create().getUniqueId();     
    scriptProperties.setProperty('wTriggerIDa', w1TrigID);
  }
     
  if(!scriptProperties.getProperty('wTriggerIDb')) {
    var w2TrigID = ScriptApp.newTrigger(strFunction).timeBased().atHour(18).nearMinute(0)
                            .onWeekDay(ScriptApp.WeekDay.WEDNESDAY).create().getUniqueId();
    scriptProperties.setProperty('wTriggerIDb', w2TrigID);
  }

  // 基本動作用トリガーを 60秒後に新規設定しつつ、セットしたトリガーのUniqueIDを取得。
  var trigId = ScriptApp.newTrigger(strFunction).timeBased().after(60000).create().getUniqueId();

  // 次回トリガーIDを プロパティ'triggerId' に保存。
  scriptProperties.setProperty('triggerId', trigId);

  return false;

}


//***** ↓↓↓ LimitedFunctions ↓↓↓ *********************************************************

//***************************************************************************************
// delaySendControls - 予約メール返信処理 
// - Created by LazwardFox - 
//
// スター付き受信メールのSubjectから日時を取得して、指定時刻到達済みメールは即返信先へ送信。
// その他の予約メールは 対象日時を全て配列に格納後、配列内で昇順ソートを実行し、
// 次の予約対象までの時間をmsec単位で このスクリプト自身のトリガーとして再設定。
//
// - Source and SetupTips -
//    http://kijtra.com/article/gmail-delay-send-by-google-apps-script/
//
// Update 20140723 091613 予約文字列の無いスター付きメールからスターを外すよう追記。
// Update 20140722 020101 廃止がアナウンスされてるMethod ScriptProperties を 
//                        PropertiesService.getScriptProperties に 差し替え。
// Update 20140720 084645 Twitter向けプロパティ未設定で Twitter投稿を無効に。
// Update 20140719 044444 Twitter投稿処理を追加。
// Update 20140718 031412 getSendTimes/delaySendReceivedMail/getIntervals/mSleep統廃合。
// Update 20140717 233137 取得時間単位を 時から分へ。
// Release 20140717 014431
//***************************************************************************************

function delaySendControls() {

  //var objTrds = GmailApp.getInboxThreads(); // 受信トレイ全体 
  var objTrds = GmailApp.getStarredThreads(); // 受信トレイ スター付きメールのみ取得 
  var len = objTrds.length; // 対象メール数取得 
  var arSendTimes = new Array();
  var Pc = -1;
  var sSetTime = 3600000; // mSec [1h] 
  
  if(!len){
    // メールがなければ 1h後にトリガーをセットして終了。
    setTrig('delaySendControls', sSetTime);
    return false;
  }

  var NowS = new Date().getTime(); // 現在時刻を取得、シリアル値に 

  for (var i = 0, l = len; i < l; i++) {

    var objMsg = GmailApp.getMessagesForThread(objTrds[i])[0]; // 単一メールオブジェクト 
    var strSubject = objMsg.getSubject(); // 件名を取得 

    // 件名から日時を抽出 
    // {yyyy/mm/dd hh:nn} ▼ 
    // var inSubjects = strSubject.match(/^(\{(\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2} \d{1,2}:\d{1,2})\}) ?(.*)?/);
    // {yyyy/mm/dd hh:nn:ss} ▼ 
    var inSubjects = strSubject.match(/^(\{(\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2} \d{1,2}:\d{1,2}:\d{1,2})\}) ?(.*)?/);

    // 件名に指定書式での日時が含まれているか。ない場合は次の対象へ 
    if (!inSubjects  || !inSubjects[1]) {
      objMsg.unstar(); // 対象が過去の送信済み予約メールであるとしてスターを外す。
      continue;
    }

    var msgDT = (new Date(inSubjects[2].replace(/\-/g,'/')+' +09:00')).getTime(); // 送信日時をシリアル値に変換 

    // 時間を取得できない。
    if(!msgDT) {
      continue;
    }

    // 現在時刻以降が指定されたメールの指定日時を配列に格納。
    if(msgDT > NowS) {
      Pc = Pc + 1;
      arSendTimes[Pc] = msgDT;
    }
    
    else { // 指定時刻が過ぎているメールは直ちに送信。
    
      if(msgDT < NowS) { 

        // ▼ 送信メールの構成を開始 
        var to = objMsg.getReplyTo();
        var subject = inSubjects[3] || '';
        var body = objMsg.getPlainBody();
        var options = {}, val;

        // 必要な情報がなければ無視 
        if (!to || !body) {
          continue;
        }

        // メール返信に先行してTweet・・・ 
        //var twiMessage = subject + body;
        //tweetInitialize();
        //twitterPost(twiMessage);
        if(tweetInitialize()) { // 2つのプロパティが未設定ならTweet処理をスルー 
          var twiMessage = subject + body; // Tweetする文字列を構成。
          twitterPost(twiMessage);
        }

        if (val = objMsg.getCc()) { // Cc 
          options['cc'] = val;
        }

        if (val = objMsg.getBcc()) { // Bcc 
          options['bcc'] = val;
        }

        if (val = objMsg.getBody()) { // HTML本文 
          if ( val.indexOf('<div')!==-1 ) { // 内容にdivタグが含まれれば。
            options['htmlBody'] = val;
          }
        }

        if (val = objMsg.getAttachments()) { // 添付ファイル 
          options['attachments'] = val;
        }

        var status = GmailApp.sendEmail(to, subject, body, options);
        if (status) {
          objMsg.moveToTrash(); // 送信したら元メールをゴミ箱へ 
        }       

      }

    }

  }

  if(!arSendTimes[0]) {

    //var sSetTime = 3600000; // 続くメールが無かった場合のトリガー指定 [1h] 

  }
  
  else {

    // 一旦配列内を昇順で並べ替え 
    arSendTimes.sort(function(a,b){
      if(a < b){return -1;}
      if(a > b){return 1;}
      return 0;
    });

    // 次回予約時刻と現在時刻との差分時間を mSecとして取得。
    var sSetTime = (new Date(arSendTimes[0]).getTime()) - (new Date().getTime());
    // setTrig('delaySendControls', sSetTime); 

  }

  // このスクリプトの次回稼動時刻までの時間を mSec単位で再設定する。 
  // 直接シリアル値でトリガーをセットする機能は スクリプトでは実装されていない。(GUIにはある) 
  setTrig('delaySendControls', sSetTime);

  var objTrds = null;
  return false;

}


//***************************************************************************************
// setTrig - 指定関数のトリガーを設定します。
// - Created by LazwardFox - 
//
// - TipsSource - 
//  https://developers.google.com/apps-script/reference/script/script-app?hl=ja#deleteTrigger%28Trigger%29
//  http://qiita.com/soundTricker/items/5a7e050a2a20f3e3938a
//
// Update 20140720 084645 初期稼動時に プロパティ'triggerId' が無くても動作するよう補完。
// Update 20140720 063750 既存対象トリガーのみ削除するよう訂正。
// Release 20140718 031412 
//***************************************************************************************

function setTrig(strFunction, sMsec) {

  // プロパティ'triggerId' に保存してある 前回設定(今回動作)分トリガーのIDを取得。
  if(delTrig = scriptProperties.getProperty('triggerId')) {
  //if(delTrig = ScriptProperties.getProperty('triggerId')) {
 
    // 既存該当トリガー削除 
    var triggers = ScriptApp.getProjectTriggers();
    for(var i in triggers) {
      if(triggers[i].getUniqueId() == delTrig) {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    }

  }

  /*
  var delTrig = scriptProperties.getProperty('triggerId');

  if(delTrig) {

    // 既存該当トリガー削除 
    var triggers = ScriptApp.getProjectTriggers();
    for(var i in triggers) {
      if(triggers[i].getUniqueId() == delTrig) {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    }

  }
  */

  // 既存トリガー全削除 ・・・運用に寄っては不向き 
  /*
  var triggers = ScriptApp.getProjectTriggers();
  for(var i in triggers) {
      ScriptApp.deleteTrigger(triggers[i]);
  }
  */

  // 指定時間(mSec)後のトリガーを新規設定しつつ、セットしたトリガーのUniqueIDを取得。
  var trigId = ScriptApp.newTrigger(strFunction).timeBased().after(sMsec).create().getUniqueId();

  // 取得したトリガーIDを プロパティ'triggerId' に保存。
  scriptProperties.setProperty('triggerId', trigId);
  //ScriptProperties.setProperty('triggerId', trigId);

  return false;

}


//***************************************************************************************
// twitterPost 他 - Twitter投稿処理。
//
// - Source and SetupTips -
//    https://sites.google.com/site/usakoyama/gastotwitterbot
// - ProblemTips - 
//    http://kurogoma.hatenablog.com/entry/2013/04/23/003225
//
// Update 20140720 084645 プロパティ'twitterAPIKey' と 'twitterAPISecret' が未設定なら
//                        Tweetしない処理に対応。
// Layouted 20140718 031412 
//***************************************************************************************

function tweetInitialize() {
  if(ScriptProperties.getProperty("twitterAPIKey") && ScriptProperties.getProperty("twitterAPISecret")) {
    // Setup OAuthServiceConfig 
    var oAuthConfig = UrlFetchApp.addOAuthService("twitter");
    oAuthConfig.setAccessTokenUrl("https://api.twitter.com/oauth/access_token");
    oAuthConfig.setRequestTokenUrl("https://api.twitter.com/oauth/request_token");
    oAuthConfig.setAuthorizationUrl("https://api.twitter.com/oauth/authorize");
    oAuthConfig.setConsumerKey(scriptProperties.getProperty("twitterAPIKey"));
    oAuthConfig.setConsumerSecret(scriptProperties.getProperty("twitterAPISecret"));
    //oAuthConfig.setConsumerKey(ScriptProperties.getProperty("twitterAPIKey"));
    //oAuthConfig.setConsumerSecret(ScriptProperties.getProperty("twitterAPISecret"));
    return true;
  }
}

function twitterPost(text) {
  // Setup optional parameters to point request at OAuthConfigService.  The "twitter" 
  // value matches the argument to "addOAuthService" above. 
  var options =
  {
    "oAuthServiceName" : "twitter",
    "oAuthUseToken" : "always",
    "method" : "POST"
  };

  // 半角のシングルクォーテーションが本文に含まれていると 再認証要求されてTweet出来なくなる。
  text = text.replace(/[']/g,"’"); // 20141214 194752

  var encodedTweet = encodeURIComponent(text).replace(/[!'()*]/g, function(c) {
    return "%" + c.charCodeAt(0).toString(16);
  });

  var result = UrlFetchApp.fetch("https://api.twitter.com/1.1/statuses/update.json?status=" + encodedTweet, options);
  var o  = Utilities.jsonParse(result.getContentText());

  Logger.log(o);
  Logger.log(result.getResponseCode());
}

function twiTest() {
  if(tweetInitialize()) {
    twitterPost("Testtext from Log");
  }
}

/*
function twiTest() {
  tweetInitialize();
  twitterPost("TestText");
}
*/

因みに、メールの返信処理のみとして利用する場合は、Twitter向けのプロパティをセットしなければ そのように動作します。
 !!!!! Update 20140720 084645 !!!!! 

Twitter向けプロパティ未設定で Twitter投稿機能無効化を可能に。
こうした GoogleAppScriptの Googleドライブへの配置と 稼動設定については、 上述の きじとらさんトコが詳しいので 参考にしてください。(丸投げw

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。