楽天市場でのお買い物用に楽天カードを所有しています。
引き落とし口座も楽天銀行にして、より多くのポイントを獲得できるようにしていますが、問題が1つあって、メインバンクが楽天銀行ではありません。そのため楽天カードで使用した金額を定期的に楽天銀行に入金すると言う作業が発生しています。
この資金移動をうっかり忘れてしまうと信用事故になってしまうので、もれなく資金移動を行えるようにご請求予定金額のご案内メールが届いたらTodoistのタスクを作成したいと思います。
手法としては、基本的に以下のシリーズ記事の焼き直しです。
GmailSearchクラスと検索演算子で対象メールを取得
まずは届いたメールから必要なテキストを抜粋します。
- Gmailの検索演算子で対象メール絞り込み
- GmailMessageクラスのメソッドで本文取得
- 正規表現で必要なテキストの抜粋
というプロセスです。
ただ、以前にも同様の機能のスクリプトを紹介しているので、今回のような焼き直し記事は、基本的にクラスを用いて実装していきます。
クラスのコードの内容がわからない場合は、元記事を参照してください。
検索演算子
まずGmailの検索演算子ですが、請求予定額お知らせメールは毎月12-13日ごろに届くため、月1回確認すればOKです。
const query = 'subject:(【楽天カード】ご請求予定金額のご案内) newer_than:1m';
メールの件名を指定して、newer_than:1mで1ヶ月以内のメールのスレッドを取得します。
GmailSearchクラスで最新のスレッドを取得
メールは月に1回の前提ですので、最新のスレッドのみを取得します。1点私のケースでの注意点は楽天カードを2枚(VISAとMaster)所有して用途によって使い分けているので、使用状況によっては2通のメールが届きます。
基本的に同日に同じ件名で届いて同一のスレッドになるので、最新スレッドのメールは全て取得する必要があります。
今回用意したGmailで検索条件を指定してマッチするメッセージを取得・操作するクラスは以下です。
/**
* class GmailSearch
* Gmailで検索条件を指定してマッチするメッセージを取得・操作するクラス
*
* プロパティ
* query - 検索条件
* threads - 検索結果の各スレッドを格納した配列
* threadMessages - 検索結果の各メッセージをスレッドごとに二次元配列に格納した配列
* allMessages - 検索結果の全メールを一次元配列に格納した配列
* lastThreadMessages - 検索結果の最新のスレッドの各メールを一次元配列に格納した配列
* firstMessages - 検索結果の各スレッドの最初のメールを一次元配列に格納した配列
*
*/
/* = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
class GmailSearch {
/**
* 検索条件から条件にマッチするメッセージを取得するコンストラクタ
* @constructor
* @param {string} query - 検索条件
*/
constructor(query) {
this.query = query;
this.threads = GmailApp.search(this.query);
this.threadMessages = GmailApp.getMessagesForThreads(this.threads);
this.allMessages = this.threadMessages.flat();
this.lastThreadMessages = this.threadMessages[0];
this.firstMessages = this.threadMessages.map(thread => thread[0]);
}
}
こうしてクラスを用意しておけば、検索条件を指定するだけで最新スレッドのメッセージを全て取得できます。
const lastThreadMessages = new GmailSearch(query).lastThreadMessages;
正規表現で接頭辞と接尾辞を指定して間の文字列を抜粋
続いてメール本文から正規表現を用いて、請求予定額のみを抜粋します。
プレーンテキストでメール本文を取得すると該当箇所は以下のような文字列になります。
[ご請求予定金額]
12,345円
接頭辞と接尾辞を指定して間の文字列を抜粋するスクリプトも以前に以下の記事で紹介しました。
- 接頭辞と接尾辞を含む文字列を抜粋
- 接頭辞と接尾辞を空の文字列に置換する
というスクリプトを以下でクラス化しています。メソッドが2種類ありますが、
- 改行非対応(1行のみ対応)で戻り値が文字列のextractStringメソッド
- 改行対応(複数行対応)で戻り値が配列のextractStringsメソッド
の2種類を用意しています。
/**
* class RegularExp
* 正規表現を利用した文字列操作に関するクラス
*
* extractString() - 引数として渡したテキスト行から指定した文字列の間にある文字列を抽出して戻り値として返すメソッド
* extractStrings() - 引数として渡した文章から指定した文字列の間にある文字列を格納した配列を戻り値として返すメソッド
*
*/
/* = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
class RegularExp {
/**
* 正規表現操作オブジェクトのコンストラクタ
* @constructor
*/
constructor() {
}
/**
* 引数として渡したテキスト行から指定した文字列の間にある文字列を抽出して戻り値として返すメソッド
*
* @param {string} lineText - テキスト行(改行含まない)
* @param {string} prefix - 抽出したい文字列の前方にある文字列
* @param {string} suffix - 抽出したい文字列の後方にある文字列
* @return {string} extString - 抽出した文字列
*/
static extractString(lineText, prefix, suffix) {
const regExp = new RegExp(prefix + '.*?' + suffix, 'g');
const extString = lineText.match(regExp)[0]
.replace(prefix, '')
.replace(suffix, '');
return extString;
}
/* = = = = = = = */
/**
* 引数として渡した文章から指定した文字列の間にある文字列を格納した配列を戻り値として返すメソッド
*
* @param {string} doc - 文章(複数行可)
* @param {string} prefix - 抽出したい文字列の前方にある文字列
* @param {Array} suffix - 抽出したい文字列の後方にある文字列
* @return {Array} extStrings - 抽出した文字列を格納した配列
*/
static extractStrings(doc, prefix, suffix) {
const regExp = new RegExp(prefix + '[^]*?' + suffix, 'g');
const strings = doc.match(regExp);
const extStrings = strings.map(string => string.replace(prefix, '').replace(suffix, ''));
return extStrings;
}
}
しかし、今回のケースではこのRegularExpクラスのメソッドがうまく機能しませんでした。理由は、抜粋したいテキストの接尾辞に含まれる[]です。
このカッコが曲者で、正規表現では特殊文字と呼ばれ、単純な文字列として指定する場合は、エスケープシーケンスを使用しないといけません。
\\[ご請求予定金額\\]
と指定することで、接頭辞と接尾辞を含む文字列を抜粋するところまではできますが、接頭辞と接尾辞を空文字列に置換しようとした際に、\\[ご請求予定金額\\] が [ご請求予定金額] とは別の文字列として認識されるので置換されません。
ということで、RegularExpクラスのメソッドを使用した置換をあきらめました。
正規表現での抜粋を2段構えに
というとでまず、接頭辞と接尾辞を含む文字列までを抜粋し、そこから数字だけを抜粋するアプローチに方針転換です。
const prefix = '\\[ご請求予定金額\\]';
const suffix = '円';
const regExpPreSuf = new RegExp(prefix + '[^]*?' + suffix, 'g');
const aryMatchedText = plainBody.match(regExpPreSuf);
とすることで、[ ‘[ご請求予定金額]\r\n12,345円’ ] という配列が得られます。今回は請求予定額お知らせメールは1通(配列の要素1つ)でした。
ここまで短く絞れるとあとは数字だけを拾えればOKです。きっと数字だけを抜粋する正規表現があるはずと検索したところ以下の記事を発見。そのままお知恵を拝借しました。
const regExpExtNum = /[^0-9]/g;
const strNum = aryMatchedText[0].replace(regExpExtNum, '');
const amount = Number(strNum);
そのまま正規表現で抜粋しただけでは文字列なのでNumber() コンストラクターを用いて数値型に変換しています。
今回は、請求予定額お知らせメールが1通だけでしたが、複数ある時のことを考えて配列内の要素を全部足し合わせる処理を加えます。
const sum = aryBillingAmount.reduce((previousValue, currentValue) => previousValue + currentValue);
単純にforループで足し合わせても良かったのですが、反復メソッドであるreduceで合計を出しています。こうして慣れていかないといつまでたっても身につきません。
ということで、ここまでの処理をまとめたスクリプトはこちらです。
function blogSumBillingAmount() {
/* 検索クエリを指定して対象メールを絞り込み */
const query = 'subject:(【楽天カード】ご請求予定金額のご案内) newer_than:1m';
const lastThreadMessages = new GmailSearch(query).lastThreadMessages; // 検索結果の最新のスレッドの各メール
/* 各メールから必要な情報を抜粋したオブジェクトを格納した配列を作成 */
const aryBillingAmount = lastThreadMessages.map(message => {
const plainBody = message.getPlainBody();
const prefix = '\\[ご請求予定金額\\]';
const suffix = '円';
const regExpPreSuf = new RegExp(prefix + '[^]*?' + suffix, 'g');
const aryMatchedText = plainBody.match(regExpPreSuf); // [ '[ご請求予定金額]\r\n12,345円' ]
// JavaScript - 文字列から数字のみを抽出する
// https://codechacha.com/ja/javascript-extract-number-from-string/
const regExpExtNum = /[^0-9]/g;
const strNum = aryMatchedText[0].replace(regExpExtNum, '');
const amount = Number(strNum);
return amount;
});
console.log(aryBillingAmount) // 請求予定金額の数値を要素とする配列
/* 複数カードの請求額を合計する */
const sum = aryBillingAmount.reduce((previousValue, currentValue) => previousValue + currentValue);
console.log(sum) // 請求予定金額の合計
}
Todoist APIでタスク作成
メールから必要な情報が抜粋できたらあとはTodoist APIでタスク作成していきます。
Todoist APIの利用もまず認証からですね。認証はこちらの記事をご参照ください。
認証が終わったらタスク作成です。これも以下の記事が参考になります。
ということでこのままだと単なる焼き直しなので、Todoist API操作もクラス化します。
- TodoistApiRequest
- TodoistProjects
- TodoistTasks
の3つのクラスを用意しました。
/**
* class TodoistApiRequest
* APIリクエストに関するクラス
*
* プロパティ
* accessToken - アクセストークン
* urlTodoist - Todoist共通APIエンドポイント
* paramsGet - GETリクエスト用パラメータ
* paramsPost - POSTリクエスト用パラメータ
*
* メソッド
* fetchResponse(url, params) - レスポンスのJSONをオブジェクトで返すメソッド
*
* Todoist公式リファレンス
* https://developer.todoist.com/guides/#developing-with-todoist
*/
/* = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
class TodoistApiRequest {
/**
* APIリクエストのための各プロパティを定義するコンストラクタ
* @constructor
*/
constructor(accessToken) {
this.accessToken = accessToken;
this.urlTodoist = 'https://api.todoist.com/rest/v1/';
this.paramsGet = {
headers: { Authorization: `Bearer ${this.accessToken}` },
method: 'get',
muteHttpExceptions: false
}
this.paramsPost = {
contentType: 'application/json',
headers: { Authorization: `Bearer ${this.accessToken}` },
method: 'post',
payload: '',
muteHttpExceptions: false
}
}
/**
* レスポンスのJSONをオブジェクトで返すメソッド
* @param {string} url
* @param {string} params
* @return {Object} JSONオブジェクト
*/
fetchResponse(url, params) {
const response = UrlFetchApp.fetch(url, params).getContentText();
return JSON.parse(response);
}
}
/**
* class TodoistProjects
* Todoistプロジェクトに関するクラス
*
* プロパティ
* apiRequest - APIリクエストオブジェクト
* url - リクエストURL
* accessToken - アクセストークン
*
* メソッド
* getProjects() - プロジェクト一覧を配列で取得するメソッド
* getProjectByName(name) - プロジェクト名からプロジェクトオブジェクトを取得するメソッド
* getIdByName(name) - プロジェクト名からプロジェクトIDを取得するメソッド
*
*/
/* = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
class TodoistProjects {
/**
* プロジェクト操作のためのリクエストURLを定義するコンストラクタ
* @parama {string} accessToken - アクセストークン
*/
constructor(accessToken) {
this.apiRequest = new TodoistApiRequest(accessToken);
this.url = this.apiRequest.urlTodoist + 'projects';
this.accessToken = accessToken;
}
/**
* プロジェクト一覧を配列で取得するメソッド
* @return {Array.<Object>} すべてのTodoistプロジェクトを格納した配列
*/
getProjects() {
const url = this.url;
const paramsGet = this.apiRequest.paramsGet;
const projects = this.apiRequest.fetchResponse(url, paramsGet);
return projects;
}
/**
* プロジェクト名からプロジェクトオブジェクトを取得するメソッド
* @param {string} name - プロジェクト名
* @return {Object} project - Todoistプロジェクトオブジェクト
*/
getProjectByName(name) {
const projects = this.getProjects();
const project = projects.find(obj => obj.name === name);
return project;
}
/**
* プロジェクト名からプロジェクトIDを取得するメソッド
* @param {string} name - プロジェクト名
* @return {number} id - プロジェクトID
*/
getIdByName(name) {
const project = this.getProjectByName(name);
return project.id;
}
}
/**
* class TodoistTasks
* Todoistタスクに関するクラス
*
* プロパティ
* apiRequest - APIリクエストオブジェクト
* url - リクエストURL
* accessToken - アクセストークン
* queries - 絞り込み条件
* objPost - タスク作成オブジェクト雛形
*
* メソッド
* getURL() - 指定した条件のタスク一覧のリクエストURLを返すメソッド
* getTasks() - タスク一覧を配列で取得するメソッド
* createNewTask(payload) - JSONオブジェクトからタスクを作成するメソッド
*/
/* = = = = = = = = = = = = = = = = = = = = = = = = = = = = */
class TodoistTasks {
/**
* タスク操作のためのリクエストURLを定義するコンストラクタ
* @parama {string} accessToken - アクセストークン
*/
constructor(accessToken) {
this.apiRequest = new TodoistApiRequest(accessToken);
this.url = this.apiRequest.urlTodoist + 'tasks';
this.accessToken = accessToken;
this.queries = {
project_id: '',
section_id: '',
label_id: '',
filter: '',
lang: '',
ids: '' // タスクIDのリスト(コンマ区切り)
};
this.objPost = {
content: '',
description: '',
project_id: '',
section_id: '',
parent_id: '',
parent: '',
order: '',
label_ids: [],
priority: '',
due_string: '',
due_date: '',
due_datetime: '',
due_lang: '',
assignee: ''
}
}
/**
* 指定した条件のタスク一覧のリクエストURLを返すメソッド
* @return {string} url - リクエストURL
*/
getURL() {
let paramsURL = '';
for (const querie in this.queries) {
const value = this.queries[querie];
if (value !== '' && paramsURL === '') {
paramsURL += `?${querie}=${value}`;
}
else if (value !== '' && paramsURL !== '') {
paramsURL += `&${querie}=${value}`;
}
}
const url = this.url + paramsURL;
return url;
}
/**
* タスク一覧を配列で取得するメソッド
* @return {Array.<Object>} aryTasks - タスク一覧を格納した配列
*/
getTasks() {
const url = this.getURL();
const paramsGet = this.apiRequest.paramsGet;
const aryTasks = this.apiRequest.fetchResponse(url, paramsGet);
return aryTasks;
}
/**
* JSONオブジェクトからタスクを作成するメソッド
* @params {Object} payload - 登録する内容のJSONオブジェクト
* @return {Object} response - 登録されたタスクオブジェクト
*/
createNewTask(payload) {
const url = this.url;
this.apiRequest.paramsPost.payload = JSON.stringify(payload);
const response = this.apiRequest.fetchResponse(url, this.apiRequest.paramsPost);
Utilities.sleep(300);
return response;
}
}
クラスの細かい説明は省略しちゃいますが、以前の記事で作成した関数をメソッドにしているようなイメージです。
今回は以下の2つのメソッドを使用しています。
- TodoistProjectsクラス getIdByName(name) – プロジェクト名からプロジェクトIDを取得するメソッド
- TodoistTasksクラス createNewTask(payload) – JSONオブジェクトからタスクを作成するメソッド
ということで完成したスクリプトはこちら。
/* 楽天カードの請求予定額を合算して口座振替タスクを作成するスクリプト */
function createTaskRakutenCard() {
/* 検索クエリを指定して対象メールを絞り込み */
const query = 'subject:(【楽天カード】ご請求予定金額のご案内) newer_than:1m';
const lastThreadMessages = new GmailSearch(query).lastThreadMessages; // 検索結果の最新のスレッドの各メール
/* 各メールから必要な情報を抜粋したオブジェクトを格納した配列を作成 */
const aryBillingAmount = lastThreadMessages.map(message => {
const plainBody = message.getPlainBody();
const prefix = '\\[ご請求予定金額\\]';
const suffix = '円';
const regExpPreSuf = new RegExp(prefix + '[^]*?' + suffix, 'g');
const aryMatchedText = plainBody.match(regExpPreSuf); // [ '[ご請求予定金額]\r\n12,345円' ]
// JavaScript - 文字列から数字のみを抽出する
// https://codechacha.com/ja/javascript-extract-number-from-string/
const regExpExtNum = /[^0-9]/g;
const strNum = aryMatchedText[0].replace(regExpExtNum, '');
const amount = Number(strNum);
return amount;
});
// console.log(aryBillingAmount) // 請求予定金額の数値を要素とする配列
/* 複数カードの請求額を合計する */
const sum = aryBillingAmount.reduce((previousValue, currentValue) => previousValue + currentValue);
// console.log(sum) // 請求予定金額の合計
/* Todoistのタスク作成先のプロジェクトを取得 */
const accessToken = getTodoistService().getAccessToken();
const projectId = new TodoistProjects(accessToken).getIdByName('Inbox');
/* 期限日を当月25日のyyyy-MM-dd形式の文字列を取得 */
const today = new Date();
const dateNext25th = new Date(today.setDate(25));
const dueDate = Utilities.formatDate(dateNext25th, 'JST', 'yyyy-MM-dd'); // yyyy-MM-dd形式の文字列に
/* Todoist APIでタスク作成 */
const payload = {
content: `楽天カード利用分口座振替|${sum}円`, // タスク名
project_id: projectId, // プロジェクトをIDで指定
due_date: dueDate // 期限日
};
new TodoistTasks(accessToken).createNewTask(payload);
}
あとはこのスクリプトを月1回、15日頃にトリガー実行するだけですね。
おわりに
今回は、楽天カードの「ご請求予定金額」お知らせメールから、Todoistに「楽天銀行への入金」タスクを作成するを紹介しました。
今回のハイライトは、正規表現でつまづいた部分ですね。結局2段階での抜粋という方法に落ち着きましたが、その過程で数字のみを抜粋する正規表現を知れたのが収穫でした。
いまどきはメールは埋もれがちなので、受信したメール起点でタスクを作成できると色んなうっかりが防げそうですね。
もしこの記事を見て、実際に活用された方がいらっしゃったらTwitterなどで感想いただけると嬉しいです。
Google Apps Scriptを勉強したい方へ
この記事を見て、GASを勉強したいなと思われた方はぜひノンプログラマーのためのスキルアップ研究会(通称 ノンプロ研)にご参加ください。私も未経験からこの学習コミュニティに参加し、講座を受講したことでGASが書けるようになりました。
学習コミュニティ「ノンプログラマーのためのスキルアップ研究会」
挫折しがちなプログラミングの学習も、コミュニティの力で継続できます。ノンプロ研でお待ちしております!
Amazon欲しい物リスト公開しています。
開発者のモチベーションアップのためにAmazon欲しい物リストを公開しております。役に立ったよ!という方の感謝の気持ちで何かいただけるのであれば嬉しいです笑