Notionからblastengine経由で送信したメールのエラーステータスを更新する

Notionは最近注目度の高い情報共有サービスです。Wikiのように自由テキストを保存したり、データベースのように構造化されたデータを保存もできます。

今回はそんなNotion内にあるメールアドレスを使って、blastengineと組み合わせてみました。前回はNotionデータベース上のメールアドレスを使ってメール送信を行いましたので、今回はエラーステータスをデータベースに反映します。

# 機能について

- データベースにある顧客リストのメールアドレス宛にメールを一括送信
- エラーリストを使って次回以降送信しないようにNotionのデータを更新

後半となるこの記事ではblastengineから取得できるエラーリストを使って、データベースにある顧客リストのメールアドレスのステータスを更新します。

ベースとなるアプリやライブラリは[前回](https://qiita.com/hirosatooooo/items/4fd1877f82741e83e60c)のものを使います。

## ファイルの作成

今回は `update.ts` というファイルを作成します。

## ライブラリ読み込み

`update.ts` への記述です。まず最初に必要なライブラリを読み込みます。

```jsx
// dotenvを読み込み、環境変数を設定する
require('dotenv').config();

// Notionのクライアントを読み込む
import { Client } from '@notionhq/client';

// Notionから返されるページオブジェクトの型を読み込む
import { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints';

// blastEngineとErrorReportを読み込む
import { BlastEngine, ErrorReport } from 'blastengine'
```

## Notionクライアントの初期化への接続情報を作成する

`.env` ファイルに定義したシークレットを使って、Notionクライアントを作成します。

```jsx
// Notion APIの認証を設定
const notion = new Client({ auth: process.env.NOTION_SECRET });
```

## blastengineの初期化

blastengine SDKを初期化します。こちらも `.env` ファイルの内容を使います。

```jsx
// blastEngineを初期化する
new BlastEngine(process.env.BLASTENGINE_USERID, process.env.BLASTENGINE_APIKEY);
```

## 無名関数の定義

ここからはネットワーク処理があるので、非同期処理用にasync/awaitを定義します。

```jsx
// メイン処理をasync関数で囲む
(async () => {
  // ここからはこの中に書いていきます
})();
```

### blastengineのエラーレポートをダウンロード

blastengineのエラーレポートをダウンロードします。これは、2回以上配信エラーがあったメールアドレスが記録されています。

```java
// blastEngineのエラーレポートを管理するオブジェクトを作成する
const report = new ErrorReport;
// ダウンロードができるようになるまで待つ
while (await report.finished() === false) {
	// 1秒待つ
	await new Promise(res => setTimeout(res, 1000));
}
// エラーレポートをダウンロードする
const results = await report.download();
```

エラーレポートは管理画面でも確認できます。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982596/5613cfbc-fb8e-3399-84b3-930dee84e977.png)

### Notionからレコードを取得

エラーレポートに含まれているメールアドレスを使って、Notionのデータベースを検索します。

```jsx
// ダウンロードしたエラーレポートからメールアドレスを取り出し、それらのメールが含まれるページのIDを取得する
const ary = await fetchAll(process.env.DB_ID!, results.map((r: any) => r.email));
```

fetchAll関数は以下のようになります。関数は `(async () => {` 外で問題ありません。

```jsx
// Notionデータベースから未送信の情報を取得する関数
const fetchAll = async (databaseId: string, emails: string[]): Promise<string[]> => {
	// データベースをクエリして、結果を取得する
	const res = await notion.databases.query({
    database_id: databaseId,
		filter: {
			// フィルタ条件は指定されたメールアドレスが含まれているエントリ
			or: emails.map(email => ({
				property: process.env.EMAIL_PROPERTY!,
				rich_text: {
					contains: email,
				}
			}))
		}
	});
	// 返された結果からIDのみを取り出す
	return (res.results as PageObjectResponse[]).map(row => row.id);
};
```

### Notionデータベースのプロパティを更新する

取得できたページIDを使って、Notionデータベースを更新します。

```jsx
// 取得したページのIDに対してプロパティを更新する
await updateAll(ary);
```

`updateAll` 関数は以下のようになります。これも `(async () => {` 外で問題ありません。

```jsx
// 全ての指定されたページのプロパティを更新する関数
const updateAll = async (ary: string[]): Promise<boolean> => {
	// 指定されたページIDすべてを非同期で更新する
	const res = await Promise.all(ary.map(id => {
		return notion.pages.update({
			page_id: id,
			properties: {
				// 更新するプロパティはCHECKBOX_IDの値
				[process.env.CHECKBOX_ID!]: {
					checkbox: true,
				}
			}
		});
	}));
	return true;
};
```

## スクリプトの全体像

`update.ｔs` の内容は以下の通りです。

```jsx
// dotenvを読み込み、環境変数を設定する
require('dotenv').config();

// Notionのクライアントを読み込む
import { Client } from '@notionhq/client';

// Notionから返されるページオブジェクトの型を読み込む
import { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints';

// Notion APIの認証を設定する
const notion = new Client({ auth: process.env.NOTION_SECRET });

// blastEngineとErrorReportを読み込む
import { BlastEngine, ErrorReport } from 'blastengine';

// BlastEngineのユーザIDとAPIキーを使ってblastEngineを初期化する
new BlastEngine(process.env.BLASTENGINE_USERID, process.env.BLASTENGINE_APIKEY);

// Notionデータベースから未送信の情報を取得する関数
const fetchAll = async (databaseId: string, emails: string[]): Promise<string[]> => {
	// データベースをクエリして、結果を取得する
	const res = await notion.databases.query({
    database_id: databaseId,
		filter: {
			// フィルタ条件は指定されたメールアドレスが含まれているエントリ
			or: emails.map(email => ({
				property: process.env.EMAIL_PROPERTY!,
				rich_text: {
					contains: email,
				}
			}))
		}
	});
	// 返された結果からIDのみを取り出す
	return (res.results as PageObjectResponse[]).map(row => row.id);
};

// 全ての指定されたページのプロパティを更新する関数
const updateAll = async (ary: string[]): Promise<boolean> => {
	// 指定されたページIDすべてを非同期で更新する
	const res = await Promise.all(ary.map(id => {
		return notion.pages.update({
			page_id: id,
			properties: {
				// 更新するプロパティはCHECKBOX_IDの値
				[process.env.CHECKBOX_ID!]: {
					checkbox: true,
				}
			}
		});
	}));
	return true;
};

// メイン処理をasync関数で囲む
(async () => {
	// blastEngineのエラーレポートを管理するオブジェクトを作成する
	const report = new ErrorReport;
	// ダウンロードができるようになるまで待つ
	while (await report.finished() === false) {
		// 1秒待つ
		await new Promise(res => setTimeout(res, 1000));
	}
	// エラーレポートをダウンロードする
	const results = await report.download();
	// ダウンロードしたエラーレポートからメールアドレスを取り出し、それらのメールが含まれるページのIDを取得する
	const ary = await fetchAll(process.env.DB_ID!, results.map((r: any) => r.email));
	// 取得したページのIDに対してプロパティを更新する
	await updateAll(ary);
	// 完了メッセージをログに表示する
	console.log('更新完了しました');
})();
```

## 実行する

以下のようにしてコマンドプロンプトやターミナルで実行します。

```jsx
npx ts-node update.ts
```

以下のようなメッセージが出れば更新されているはずです。

```jsx
更新完了しました
```

Notionデータベースも更新されているはずです。

![Untitled 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982596/37e99dee-5aa5-20aa-0174-dc690341d84e.png)


まとめ

今回はblastengineの配信エラーレポートをダウンロードして、Notionのデータベースへ反映するまでの流れを紹介しました。

Notionデータベースはメールの配信ログやメールアドレスを蓄積しておくのにぴったりです。ぜひblastengineと組み合わせてください。

[エンジニア向けメール配信システム「ブラストエンジン（blastengine）」](https://blastengine.jp/)