ブラストメールAPIをGASで操作する
ブラストメールとLINE公式アカウントを利用した緊急連絡システムを作ったという話を、【構想編】、【設計編】、【環境編】、【コード全体編】として、総論の話をしてきました。今回からはコードの中身、各論に入っていきたいと思います。
ブラストメールAPIの全体像
ブラストメールのAPIの仕様は、ホームページ上に公開されています。使える機能は以下のようになっています。
このように、一通りの情報をAPIで取得できるようになっています。
ログイン
ブラストメールAPIで各種機能を使用する際、まず最初にログインしてアクセストークンを取得する必要があります。ここで取得するアクセストークンは長期間保有されず、リクエストのたびに違う文字列に変更されます。他の機能を利用する際は、ここで取得したアクセストークンをパラメータに付けてリクエストするわけですね。
細かい仕様を見ていくと…
①https://api.bme.jp/rest/1.0/authenticate/loginに対し、
②username,password,api_key,fのパラメータをつけ、
③POSTリクエストを送る
ということですね。
GASコードにすると以下のようになります。
function blastmailLogin() {
var url = "https://api.bme.jp/rest/1.0/authenticate/login";
var postData = {
"f": "json",
"username": "*********",
"password": "*********",
"api_key": "**********"
};
var options = {
"method": "POST",
"payload": postData,
"muteHttpExceptions":true
};
var response = UrlFetchApp.fetch(url, options);
var data = JSON.parse(response);
return data.accessToken;
}
①1行目でURLを指定し、
②postDataにパラメータを格納、
③UrlfetchでPOSTリクエスト
という流れです。
パラメータに使用するusernameとpasswordは、アカウントを作る際にサポートの方からメールで送られてきて、ブラウザからログインするときにも使用します(パスワードは後で変更可能です)。そしてapi_keyは、アカウント開設後にアカウント情報のページから確認できるようになっています。
この3つの情報は決して外部に漏れないようにしてください。
ログアウト
続いて、ログアウトの処理です。仕様は以下の通りです。
こちらも簡単なつくりになっています。access_tokenをパラメータにつけてGETリクエストを送るだけです。これに関してはレスポンスも必要ないと思うので、レスポンス形式も指定していません。
function blastmailLogout(token) {
var url = "https://api.bme.jp/rest/1.0/authenticate/logout"
var requestQuery = `?access_token=${token}`;
url = url + requestQuery;
UrlFetchApp.fetch(url);
}
こう、できてしまえばとても簡単だったのですが、実はここで私つまづきました。GETリクエスト時のパラメータのして方法がわかっていなくて、payloadにをoptionsにつけて送ろうとしていました(もちろんエラーが出ます)
payloadはGETリクエスト時は使えないそうで、URLにクエリ文字列を加えることになっている、とサポートの方に教えていただきました。たぶんすごくレベルの低い話でサポートの方もげんなりしながら対応してくださったのだと思いますが、とても丁寧なご対応でございました。
クエリ文字列の書き方は、URLに「?」とパラメータを足すだけです。パラメータが複数ある場合は、パラメータの間に「&」を入れます。これだけです。
そういえば、google検索からHPに飛ぶとURLがやたら長かったり、アフィリエイトの商品ページのURLが長いのもこうやってパラメータを色々くっつけているのね!?と色々得心がいったのでした。
ログアウト機能は、API操作の一番最後に使用します。例えば、
①ログイン → ②メール配信 → ③ログアウト
という具合です。ログイン時に得たaccess_tokenを②に渡し、最終的に③でログアウトして一連の処理が終わる。別の処理を始めるときはまた①から始める。という流れになります。しかしこのログアウト機能、要らないっちゃ要らないような気もします。なぜなら、③を省略しても①から新しい処理が始められる仕様だからです。どちらでも良いと思います。
メール即時配信
ここからブラストメールの機能を使っていきます。メールの即時配信の方法を見ていきましょう。
パラメータが増えましたが、理屈は同じです。access_tokenはログインして取得し、senderIDはAPIで送信元アドレス一覧を取得、groupIDもAPI操作で取得できます。私はグループを複数作っていなかったのと、送信元アドレスも1つだけだったので、IDはそれぞれ「1」と指定すればOKです。
メールの送信内容はGoogleフォームで記入するのですが、題名、本文、送信者名と3つの記入欄を用意しています。
メールを送る際、本文の末尾に送信者名を付け、送信します。したがって、以下のようなコードになります。
function sendBlastmailNow(subject,text,name) {
var text = text + "\n\n会社名****************\n" + name
var url = "https://api.bme.jp/rest/1.0/message/sendnow/create";
var token = blastmailLogin()
var postData = {
"format": "json",
"senderID": "1",
"groupID": "1",
"subject": subject,
"textPart": text,
"access_token": token,
"public":"true"
};
var options = {
"method": "POST",
"payload": postData,
"muteHttpExceptions":true
};
var response = UrlFetchApp.fetch(url, options);
var jsonData = JSON.parse(response)
var messageId = jsonData.messageID
unregistered(messageId,token)
}
今度はPOSTリクエストなのでpayloadにパラメータを載せています。
ここで一つ注意点があります!上記の仕様ではレスポンス形式を指定する際に"f"と記述することになっていますが、実際には"format"としないとエラーが出ます。これはリファレンスページの誤りだとサポートの方がおっしゃっていました。
メールを配信すると、メッセージIDという通し番号がレスポンスとして返ってきます。これをunregisteredという未登録者や送信失敗リストとの照合を行う関数に渡しています。これについてはまた別の機会に説明できればと思います。
ユーザーのリストを取得する
ブラストメールを利用している方のリストを取得するAPIです。LINE側のユーザーリストと合わせることで、誰が登録していて誰がしていないのか、全体をリスト化できます。
ブラストメールAPIにはユーザー情報を取得する方法として、「読者データの検索」と「読者データの取得」の種類があります。今回は「取得」の方を利用します。違いとしては、前者はデータをJSONで受け取れる一方、最大100件しか情報を取得できません。条件を絞り込んで特定の利用者を選定する必要がある場合に向いていると思います。後者はデータをCSVで受け取ります。大量のデータをリストにするときはこちらの方が向いていると思います。
今回は全データが含まれたリストが欲しいので、パラメータはaccess_tokenだけでOKです。GETなのでクエリパラメータにしてリクエストします。
function getBlastmailUser() {
var url = "https://api.bme.jp/rest/1.0/contact/list/export";
var token = blastmailLogin()
var requestQuery = `?access_token=${token}`;
url = url + requestQuery;
var response = UrlFetchApp.fetch(url);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
blastUserSheet.getRange(1,1,blastUserSheet.getLastRow(),blastUserSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
blastUserSheet.getRange(2, 2, csv.length, csv[0].length).setValues(csv);
blastUserSheet.getRange(1,1).setValue("Blastmailユーザー一覧("+ Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + " 現在)")
blastUserSheet.getRange(2,1).setValue("No.")
for(var i = 3 ; i<=blastUserSheet.getLastRow() ; i++){
blastUserSheet.getRange(i,1).setValue(i-2);
}
blastmailLogout(token)
}
取得したCSVをシートに転記するのですが、そのまま転記すると文字化けします。なぜ文字化けするかというと、ブラストメールで採用している文字コードがshift-jisという規格だからみたいです。イマドキの多くのサイトやアプリではutf-8という規格を採用しているのになぜ?と思ったのですが、shift-jisという文字コードは、携帯電話で多く採用されている規格のようで、ブラストメールからutf-8の文字コードでメールを送信すると携帯電話の受信者が文字化けだらけのメッセージを受信することになってしまうため、shift-jisを採用しているのだと思います。勝手な予想ですが、当たらずとも遠からずではないかと。
この問題は.getContentText("shift-jis")と記述することで文字コードを変換してアッサリ解決したのですが、後ほど別の問題が生じることになりました。それについてはまた後日。
実行すると、上記のようなリストが出力されます。このリストは定期的に最新の情報に更新したいので、トリガー設定で1hに1回とか(環境に合わせて変えてOK)動くようにしておきます。私は1日1回の設定にしています。
この機能を作った2020年12月当初、この機能を使うと原因不明のエラーが発生することが頻繁にありました。どうやらコードの問題ではなく、GAS側のエラーだったようです。エラーは年明けごろから回数が減っていったのですが、今でもごく稀に発生しているようで、その際にこちらのシステムにエラーが波及しないようにtry-catchを入れています。tryに入れているのは、シート全体の値を消去する部分なのですが、前回のUrlfetch時にエラーが出ると、シートがまっさらになったまま次のトリガーを待つ状態になります。そして次のトリガー時に「消去するものがない」とエラーが発生してしまうのでそれを防止する、ということです。この記事を書いていて思いましたが、if文の方が意図がわかりやすかったかもしれません。
登録推移情報の取得
次に、登録者の推移を取得する方法です。こちらもJSONで取得するかCSVで取得するか選べるのですが、前者はやはり100日分までしか取得できないので、CSVで受け取ります。
パラメータにはトークンの他に開始日付(私は利用開始日を指定)と終了日(私は今日を指定)を指定します。日付の形式は上記の通りにしないとエラーとなります。
function getBlastmailStatusLog() {
var url = "https://api.bme.jp/rest/1.0/statuslog/list/export";
var token = blastmailLogin();
var date = new Date(2020,11,9);//利用開始日
var startDate = Utilities.formatDate(date, 'Asia/Tokyo', "yyyyMMdd'T'HH:mm:ss");
var today = Utilities.formatDate(new Date(), 'Asia/Tokyo', "yyyyMMdd'T'HH:mm:ss");
var requestQuery = `?access_token=${token}&beginDate=${startDate}&endDate=${today}`;
url = url + requestQuery;
var response = UrlFetchApp.fetch(url);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
blastStatuslogSheet.getRange(1,1,blastStatuslogSheet.getLastRow(),blastStatuslogSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
blastStatuslogSheet.getRange(2, 1, csv.length, csv[0].length).setValues(csv);
blastStatuslogSheet.getRange(1,1).setValue("Blastmail登録者推移("+ Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + " 現在)");
blastmailLogout(token)
}
今回はクエリ文字列に複数のパラメータを設定するので、
var requestQuery = `?access_token=${token}&beginDate=${startDate}&endDate=${today}`;
このように&で連結してurlに足しています。
ちなみに文字列や変数を連結する際、”文字列”+【関数】+・・・と書いていくのが基本だと思いますが、上記の例で行くと
var requestQuery = "?access_token=" + token + "beginDate=" + startDate + "endDate=" + today;
こんな感じで長くて読みづらい感じになっちゃいます。
前者のような書き方はテンプレートリテラルといって、覚えると断然こっちのほうが気持ちよく書けます。おすすめです。
送信失敗アドレスの取得
緊急のメールを配信した後、気になるのはどこまでメールが配信できたか、です。情報が伝わっていない場合にはフォローが必要になります。そのため、エラーが発生している場合にはそれを送信者に通知する機能が必要になるのです。
先程メール配信のコードを書いた際、最後にunregisteredという関数にmessageIDというメールの通し番号を渡しました。ここで書くコードは、unregisteredから呼び出され、メール配信に失敗した宛先を渡すコードです。
function getFailed(messageId,token) {
var nameArray = [];
var numberArray = [];
var url = "https://api.bme.jp/rest/1.0/history/list/export";
var requestQuery = `?access_token=${token}&messageID=${messageId}&status=1`;
url = url + requestQuery;
var response = UrlFetchApp.fetch(url);
var data = response.getContentText("shift-jis");
var csv = Utilities.parseCsv(data);
try{
failedMailSheet.getRange(1,1,failedMailSheet.getLastRow(),failedMailSheet.getLastColumn()).clearContent()
}catch(e){
var result = "エラーの内容="+e;
Logger.log(result)
}
failedMailSheet.getRange(2, 2, csv.length, csv[0].length).setValues(csv);
failedMailSheet.getRange(1,1).setValue("Blastmail送信失敗宛先一覧(messageID : "+ messageId + " 送信日 : " + Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd') + ")")
failedMailSheet.getRange(2,1).setValue("No.")
for(var i = 3 ; i<= failedMailSheet.getLastRow() ; i++){
failedMailSheet.getRange(i,1).setValue(i-2);
}
for(var i = 3; i <= failedMailSheet.getLastRow(); i++){
var address = failedMailSheet.getRange(i,2).getValue();
var addressFinder = listSheet.createTextFinder(address).findAll();
for(var j = 0; j < addressFinder.length; j++ ) {
var name = addressFinder[j].offset( 0 , -2).getValue();
var number = addressFinder[j].offset( 0 , 2).getValue();
nameArray.push(name)
numberArray.push(number)
}
}
var failedData = {"name": nameArray,"number":numberArray}
return failedData
}
これこそJSONで受け取れると良いのですが、仕様がCSVなので仕方ありません。データを操作する関係で複雑になっていますが、最終的にはエラーが出た人の氏名と緊急連絡先の電話番号が配列で返されるようになっています。
黒塗りばかりでアレですが、「リストシート」にはLINEやメール両方のデータが統合されるようになっており、緊急時には電話番号から電話できるよう登録しておきます。
「blastmail送信失敗シート」には、何らかの理由でメール配信が失敗したアドレスが並びます。ここに表示されたアドレスを「リスト」シートと照合し、その方の名前と電話番号を取得しています。
データを返す際は、複数のデータが含まれることや、操作のしやすさから、{}オブジェクトの中に配列を格納して返しています。
送信エラーについて
ここで送信エラーについて触れたいと思います。
ブラストメールと契約するにあたって、私の施設では安価な3000円のコースを選択しました。このコースには迷惑メール対策機能(DKIM署名)がついておらず、6000円のコースから付くようになっています。
DKIM署名については、ブラストメールのHPに詳細が載っています。
設定されていることにより送信メールの信頼度が上がり、受信側で迷惑メール扱いされにくくなるという利点があります。
ということですが、では設定していないとどのようなことが発生するのか
事例①:迷惑メールフォルダに仕分けられる
メールが送れているはず(エラーになっていない)だが、利用者からメールが来ないと言われる。確認してもらうと、迷惑メールフォルダに入っていたという事例が多数ありました。特にGmailを利用されている方はこの例が多かったです。
事例②:迷惑メールフィルタで弾かれる
通信事業者各社の基準で、自動的に弾かれるフィルタがあるようで、auを利用されている方の多くがこれで弾かれました。ただ弾かれなかった人もいるし、突然メールが届かなくなったり、何日も後に届いたり、様々な現象が発生しました。
【解決法】
各メールサービスの選択受信設定でメールを受け取る設定を各家庭に行なってもらいました。家庭によってはこちらで操作を手伝うなど、なかなか大変ではありました。会員数が多い場合などは、DKIM署名は必須かもしれません。
ちなみに
auの話が出たのでついでにお伝えすると、通常メールを送ってエラーが発生した場合、エラーメッセージが返ってくるのが通常なのですが、auへのメールでエラーが出てもエラーは返ってこないことが多いようです。なので、今回エラーをシートに転記するコードを書いていますが、auのエラーは表示されません。なので常にその可能性があることに留意して置く必要があるといえるでしょう。
バックナンバーの取得
過去に送ったメールを後から見返す機能です。Blastmailの管理画面には、実はバックナンバーページが表示されるURLが載っていて、それをメールに添付すればユーザーはWEB上でバックナンバーを見られる仕組みになっています。しかしそのURLのクエリパラメータを見ると・・・i=〇〇とかno=1とかいう部分があり、パラメータをいじると他のアカウントやグループへのメールまで見られてしまうという危険な仕様だということがわかったので、この機能は使わないことにしました。
また、このようにURLをメール本文に載せると通常より迷惑メール判定を受ける可能性があがるそうで、ただでさえDKIM署名を利用せず判定を受けやすいのに、URLを載せることで更に確率が上がってしまうのでは、デメリットが大きすぎます。したがって、メールではバックナンバーの公開はしないことにしました。まあメールはBOX内でソートとか使えば見やすいですし、いいかなと思っています。
逆にLINE上では、過去のメッセージをさかのぼって見るのは、意外と難しいです。もともとLINEは長文を読むのには不向きなので、過去のメッセージをスクロールしながら探すのも少し面倒だったりします。そこをこのAPIを使って見やすくしようというのが狙いです。
ブラストメールを使っていないLINEユーザーがブラストメールのAPIからメッセージを取得するのも変な話なのですがまあいいでしょう
このコードはLINEのアクションと連動させるので、スプレッドシート側に書きます。
function getBackNumber(token) {
var itemArray = [];
var length;
var message = getBacknumberMessage()
if(message.length>13){
length = 13
}else{
length = message.length
}
for(var i = 0; i < length; i++){
var date = message[i].date
var year = Number(date.slice(0,4))
var month = Number(date.slice(4,6))
var day = Number(date.slice(6,8))
var hour = Number(date.slice(9,11))
var minute = Number(date.slice(12,14))
var newDate = new Date(year,month-1,day,hour,minute)
var object = {"type":"action","action":{"type":"postback","label":`${month}/${day} ${hour}:${minute}`,"data":date,"displayText":`${month}/${day} ${hour}:${minute}のメッセージ`}}
itemArray.push(object)
}
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
'headers': {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + channel_access_token,
},
'method': 'POST',
'payload': JSON.stringify({
"replyToken": token,
"messages": [{
"type": "text", // ①
"text": "過去に送信したメッセージを再確認できます(最新13通分まで)\n左右にスクロールし、日時を選択してください",
"quickReply": { // ②
"items": itemArray
}
}],
}),
});
}
function getBacknumberMessage(){
var token = blastmailLogin()
var url = "https://api.bme.jp/rest/1.0/message/backnumber/detail/search";
var requestQuery = `?access_token=${token}&f=json&groupID=1`;
url = url + requestQuery;
var options = {"muteHttpExceptions":true};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response)
return json.message
}
このコードは2つの関数からできています。
getbacknumber()は、ユーザーが「配信履歴」と入力すると起動する仕掛けです(リッチメニューで実現します)
getbacknumber()はgetBacknumberMessage()を起動し、APIを使って過去のメッセージ内容をGETします。ちなみにバックナンバーのAPIを利用するには、メール配信の際にpublic=trueのパラメータを持たせないといけません。
次に、LINEMessagingAPIのクイックリプライ機能を使い、最新13件分のメッセージを表示できるようにしています。
実際にはこんな感じで動きます。
メッセージが増えてきたら、クイックリプライを二重に使って月を選ぶとその月に発行されたメッセージが表示されるのも良いかもしれません。
以上で今回のシステムに利用したブラストメールAPI関連のコード解説を終わります。すっごく長くなっちゃいました。