忙しくて未読なメルマガの要点をNotionQAで #Notion

NotionQAで色々試すアドベントカレンダー13日目です。積読になりがちなメールマガジンもNotionに転送してAIに概要を出してもらうだけで、スルスルと読むキッカケになります。
2023.12.13

NotionQAで色々試すアドベントカレンダー13日目です。

購読した当初はじっくり読んでいたメールマガジンも、時期が経つに連れて未読積読になりがちです。

面倒でそのままさらっと削除しがちですが、NotionにアップしてNotionQAで要点を問いただせば手っ取り早いのではと思い、試してみました。

GAS経由でGmailからNotionへ

ベースのコードはChatGPTに質問しました。返答で出されるコードの実装時期はやや古いことが多く、差分でエラーとなった箇所に修正を入れています。

必要な設定は3点。Notionコネクトでの連携追加と、Notionへの転送対象にするGmailラベルの設定、転送先データベースです。ラベルは「メールマガジン」の下にサブで区切ったものを使いました。

GASで実装するコード

Notion内にページを自動生成したい場合、ページを作成した後にページIDを元にブロック追加にて本文を足していきますが、NotionのテキストブロックはAPIによるリクエストに限り、1ブロックあたり2000文字の追記が上限となっています。

メールマガジンは基本的にテキスト分量が多めです。もしも上限に掛かるようであれば、何らかの条件をもって区切りましょう。

データベースID、token、Gmailのラベルをそれぞれ実際のものに書き換えます。メールマガジンのラベルが複数ある場合は、必要な個数だけOR判定を入れて下さい。

// GASでNotion APIを使用するためのヘッダー
const headers = {
  'Authorization': 'Bearer secret_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  'Content-Type': 'application/json',
  'Notion-Version': '2022-06-28', // Notion APIのバージョン
};

// NotionデータベースのID
const notionDatabaseId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

// Gmailのラベル
const gmailLabel = '(メールマガジン-XXXXXX|メールマガジン-XXXXXXXX) ';
const tag = {
  'xxxxxxxx <xxxxx@xxxxxxxx.com>': ["tag1"],
  'yyyyyyyy <yyyyyy@yyyyyyy.com>': ["tag2"]
}
function processEmails() {
  // Gmailからメールを取得
  let before = new Date()
  before.setDate(before.getDate() - 1)
  Logger.log(Utilities.formatDate(before, 'Asia/Tokyo', 'yyyy/MM/dd'))
  const threads = GmailApp.search(
    Utilities.formatString('label:%s after:%s', gmailLabel,
      Utilities.formatDate(before, 'Asia/Tokyo', 'yyyy/MM/dd')
    )
  );
  // 取得したメールを処理
  threads.forEach(thread => {
    const messages = thread.getMessages();
    messages.forEach(message => {
      const subject = message.getSubject().replace('【メールマガジン】', '');
      const body = message.getPlainBody();

      // Notionにデータを追加
      const page = addToNotionDatabase(subject, body);
      const bodyBlock = appendPageText(JSON.parse(page), body)
      const updateProperties = updateTags(JSON.parse(page), message.getFrom())
    });

    // メールを処理したらラベルを削除(任意)
    //thread.removeLabel(GmailApp.getUserLabelByName(gmailLabel));
  });
}

// Notionにデータを追加する関数
function addToNotionDatabase(subject) {
  const notionPageEndpoint = `https://api.notion.com/v1/pages`;

  const data = {
    parent: {
      database_id: notionDatabaseId,
    },
    properties: {
      title: {
        type: 'title',
        title: [
          {
            text: {
              content: subject,
            },
          },
        ],
      }
    },
  };

  const options = {
    method: 'post',
    headers: headers,
    payload: JSON.stringify(data),
  };

  // Notionにデータを追加
  return UrlFetchApp.fetch(notionPageEndpoint, options);
}

function appendPageText(page, text){
  var data = {
    "children": []
  }
  let maxLineCount = text.split("\n").length
  let lineCount = 1
  text.split("\n").forEach(function(line) {
    lineCount += 1
    if (line.replace(' ', '').trim().length == 0){
      return;
    }
    data["children"].push({
        "type": "paragraph",
        "paragraph": {
          "rich_text":[{
            "type": "text",
            "text": {
              "content": line,
              "link": null
            },
            "plain_text": line,
                "href": null
          }],
          "color": "default"
        }
      })
    if (data["children"].length == 100) {
      postBody(page['id'], data)
      data["children"] = []
    }
    if (lineCount >= maxLineCount && data["children"].length > 0) {
      Logger.log(data)
      let _result = postBody(page['id'], data)
    }
  });
  let _result = postBody(page['id'], data)
  Logger.log(_result)
}

function postBody(page_id, data) {
  const options = {
    method: 'patch',
    headers: headers,
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };
  const notionBlocksEndpoint = Utilities.formatString(
    'https://api.notion.com/v1/blocks/%s/children', page_id);
  // Notionにデータを追加
  return UrlFetchApp.fetch(notionBlocksEndpoint, options);
}

function updateTags(page, from) {
  let data = {
    "properties": {
      "タグ": []
    }
  }
  Logger.log(from)
  tag[from].forEach(tag => {
    data["properties"]["タグ"].push({"name": tag})
  })
  const options = {
    method: 'patch',
    headers: headers,
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };
  const notionBlocksEndpoint = Utilities.formatString(
    'https://api.notion.com/v1/pages/%s', page['id']);
  // Notionにデータを追加
  return UrlFetchApp.fetch(notionBlocksEndpoint, options);
}

実行すると以下のようになります。


本文をNotionに合わせてフォーマットする

NotionQAに対して本文を加工しながら問い合わせてみたところ、見出し設定、リスト化、AIによる数値の全角半角置換など行った後のほうが適切な返答がなされるようでした。

なお、AIに対して全角数値の半角化を依頼する場合は以下のように指示することで処理してくれます。

全角数字を半角数字に直して

QAしてみる

本題の、メルマガ要点を質問しました。Mac上でのQAが起動エラーになったため、スマホアプリ版です。必要に応じて本文を読めるところもポイントですね。

あとがき

メール全般の転送も試みましたが、HTMLメールで画像に字が埋まっていたり、文章というよりもキーワードの山だったりと、NotionQAによる判定には向かないメールが多かったので、メールマガジンに絞ってみました。

転送すると、今度は本文の要素をNotionのフォーマットに合わせて加工するのが大変なのですが、済んでしまえばNotionQA向けリソースとなります。踏ん張りどころです。