脳に優しく、情報収集する方法【落ち着いた心から、世界を見る】

こんにちは、マナブです。
情報収集に関する話です。

基本的に、今の世の中の人は「脳がオーバーロード」している。SNSやLINE、夜でもディスプレイを見たり、スマホゲーなど。

そのまま生きても良いけど、結局は「非効率」です。疲れているのに筋トレをするのと同じで、休む時に筋肉が成長する事を忘れている。

じゃあ、どうするか? Kindleです。


テック界隈でも、増えている。

これは「当たり前の流れ」で、なぜなら共通ゴールは「生産性と気分を上げつつ、効率的に働く」だから。

なので最近は「1時間のオフィスワークをしたら5分の散歩」とか「スマホを白黒画面にして、脳の疲れを減らす人」が増えてる。

この文脈でKindleが最適で、解説します。


届いたメルマガを、自動転送。

論より証拠で、僕のワークフローは「読みたいメルマガをKindleに自動転送 → Kindleで、ゆっくり読書」です。

Kindle画面だと脳が疲れづらく、かつ僕は長文の英語記事を読む事が多く、Kindle読書で英語力も大幅に伸びた。方法は下記です。

const CONFIG = {
  // あなたのSend to Kindleアドレス(Amazonの「コンテンツと端末の管理」で確認)
  KINDLE_EMAIL: '___YOUR_KINDLE_EMAIL_HERE___',

  // Kindleに送りたいメルマガの差出人アドレス(ここに追加・削除するだけで管理できる)
  SENDERS: [
    '____NEWSLETTER’S_SENDER_EMAIL___',
    '____NEWSLETTER’S_SENDER_EMAIL___',
    // '____NEWSLETTER’S_SENDER_EMAIL___',
  ],

  // 何日前までのメールを検索対象にするか(トリガー間隔より十分長ければOK)
  SEARCH_WINDOW_DAYS: 2,

  // 処理済みメールIDを何日分記憶しておくか(SEARCH_WINDOW_DAYSより長くすること)
  PROCESSED_RETENTION_DAYS: 7,

  // 自動実行の間隔(分)。1, 5, 10, 15, 30 のいずれか
  TRIGGER_INTERVAL_MINUTES: 15,
};

/**
 * メイン処理。時間トリガーから定期実行される。
 */
function main() {
  if (CONFIG.SENDERS.length === 0) {
    console.warn('CONFIG.SENDERS が空です。対象メルマガの差出人を登録してください。');
    return;
  }
  if (CONFIG.KINDLE_EMAIL.indexOf('@kindle.com') === -1) {
    console.warn('CONFIG.KINDLE_EMAIL がSend to Kindleアドレスではありません: ' + CONFIG.KINDLE_EMAIL);
    return;
  }

  const processed = loadProcessedIds_();
  const query = buildSearchQuery_();
  const threads = GmailApp.search(query);
  let sentCount = 0;

  threads.forEach(function (thread) {
    thread.getMessages().forEach(function (message) {
      const id = message.getId();
      if (processed[id]) return; // 送信済み
      if (!isTargetSender_(message.getFrom())) return; // スレッド内の無関係なメールを除外

      try {
        sendToKindle_(message);
        processed[id] = Date.now();
        sentCount++;
        console.log('Kindleへ送信: ' + message.getSubject());
      } catch (e) {
        // 失敗したメールは処理済みにせず、次回の実行で再試行される
        console.error('送信失敗: ' + message.getSubject() + ' — ' + e);
      }
    });
  });

  saveProcessedIds_(processed);
  console.log('チェック完了。新規送信: ' + sentCount + '件');
}

/**
 * 1通のメールを .html 添付ファイルにしてKindleアドレスへ送信する。
 * Amazonの変換エンジンはHTML内のリモート画像を取得してくれないため、
 * 画像をこちらでダウンロードし、base64データURIとしてHTMLに直接埋め込む。
 * (ZIP同梱方式は、Amazonが中の各ファイルを別々のドキュメントとして
 *   変換してしまうため使えない)
 */
function sendToKindle_(message) {
  const subject = message.getSubject() || '(無題)';
  let html = buildDocumentHtml_(message, subject);
  const filename = sanitizeFilename_(subject) + '.html';

  // リモート画像をダウンロードしてHTMLに埋め込む。
  // 取得できない画像(非対応形式・エラー)はKindle上でリンク切れアイコンに
  // なるだけなので、タグごと取り除く。
  let embeddedCount = 0;
  let embeddedBytes = 0;
  html = html.replace(/<img src=("|')(https?:\/\/[^"']+)\1([^>]*)>/g, function (match, quote, url, rest) {
    if (embeddedCount >= 30 || embeddedBytes > 20 * 1024 * 1024) return ''; // 安全上限
    const dataUri = fetchImageAsDataUri_(url);
    if (!dataUri) return '';
    embeddedCount++;
    embeddedBytes += dataUri.length;
    return '<img src="' + dataUri + '"' + rest + '>';
  });

  const blob = Utilities.newBlob('', 'text/html', filename)
    .setDataFromString(html, 'UTF-8');

  GmailApp.sendEmail(CONFIG.KINDLE_EMAIL, subject, 'Sent by auto-send-newsletter-to-kindle', {
    attachments: [blob],
    name: 'Newsletter to Kindle',
  });
}

/**
 * 画像URLをダウンロードして data:image/...;base64,... 形式の文字列にする。
 * Kindleが扱えない形式(webp/svg等)や取得失敗時はnullを返す。
 */
function fetchImageAsDataUri_(url) {
  const SUPPORTED_TYPES = {
    'image/jpeg': true,
    'image/jpg': true,
    'image/png': true,
    'image/gif': true,
    'image/bmp': true,
  };
  try {
    const resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (resp.getResponseCode() !== 200) return null;
    const blob = resp.getBlob();
    const type = (blob.getContentType() || '').toLowerCase().split(';')[0];
    if (!SUPPORTED_TYPES[type]) return null;
    const bytes = blob.getBytes();
    if (bytes.length > 5 * 1024 * 1024) return null; // 1枚5MBまで
    return 'data:' + type + ';base64,' + Utilities.base64Encode(bytes);
  } catch (e) {
    return null;
  }
}

/**
 * メール本文からKindle変換用のHTMLドキュメントを組み立てる。
 * プレーンテキストのメール(まぐまぐ等のテキストメルマガ)は、
 * 改行を<br>に、空行を段落区切りに変換してからHTML化する。
 */
function buildDocumentHtml_(message, subject) {
  let body = message.getBody(); // HTML本文
  if (!body || !looksLikeHtml_(body)) {
    body = plainTextToHtml_(message.getPlainBody() || '');
  } else {
    body = cleanEmailHtml_(body);
  }

  const from = escapeHtml_(message.getFrom());
  const date = Utilities.formatDate(message.getDate(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');

  return (
    '<!DOCTYPE html>' +
    '<html lang="ja">' +
    '<head>' +
    '<meta charset="UTF-8">' +
    '<title>' + escapeHtml_(subject) + '</title>' +
    '</head>' +
    '<body>' +
    '<p style="color:#666;font-size:0.85em;">' + from + '<br>' + date + '</p>' +
    '<hr>' +
    body +
    '</body>' +
    '</html>'
  );
}

/**
 * メールのHTMLをKindle変換用にクリーンアップする。
 * SubstackなどのメルマガHTMLは、深い入れ子のテーブル・大量のインラインスタイル・
 * 条件付きコメントを含み、Amazonの変換エンジンがこれらを誤解釈して
 * 本文の大部分を落とすことがある(冒頭とフッターだけ残る症状)。
 * 対策として、本文の意味を持つタグ(段落・見出し・リンク・画像など)だけを
 * 白リストで残し、レイアウト用のタグと属性をすべて取り除いた
 * シンプルなHTMLに再構築する。
 */
const KEEP_TAGS_ = /^(?:p|br|hr|h1|h2|h3|h4|h5|h6|a|img|ul|ol|li|blockquote|strong|em|b|i|u|s|small|sub|sup|pre|code)$/;

function cleanEmailHtml_(html) {
  let cleaned = html;

  // HTMLコメントを除去(Outlook向け条件付きコメント <!--[if mso]> ... を含む)
  cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, '');

  // DOCTYPE宣言を除去(後段の白リスト処理はタグのみ対象のため個別に消す)
  cleaned = cleaned.replace(/<!DOCTYPE[^>]*>/gi, '');

  // <head>(<meta>や<style>入り)、独立した<style>/<script>を除去
  cleaned = cleaned.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head\s*>/gi, '');
  cleaned = cleaned.replace(/<style(\s[^>]*)?>[\s\S]*?<\/style\s*>/gi, '');
  cleaned = cleaned.replace(/<script(\s[^>]*)?>[\s\S]*?<\/script\s*>/gi, '');

  // SubstackのCDN変換URLを元画像の直接URLに戻す(Amazonが取得できず画像が壊れる対策)
  cleaned = cleaned.replace(
    /https:\/\/substackcdn\.com\/image\/fetch\/[^"'\s>]*\/(https%3A%2F%2F[^"'\s>]+)/gi,
    function (match, encodedUrl) {
      try {
        return decodeURIComponent(encodedUrl);
      } catch (e) {
        return match;
      }
    }
  );

  // 開封トラッキングピクセルを除去(1px画像、Substackの /o/ ビーコン)
  cleaned = cleaned.replace(/<img[^>]*\s(?:width|height)=["']?1["']?[^>]*>/gi, '');
  cleaned = cleaned.replace(/<img[^>]*substack\.com\/o\/[^>]*>/gi, '');

  // タグを白リスト化: 本文系タグ以外(table/div/span等)は取り除き、中身のテキストだけ残す。
  // 残すタグも属性を落とす(<a>のhrefと<img>のsrc/altだけ保持)。
  cleaned = cleaned.replace(
    /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g,
    function (match, slash, tag, attrs) {
      tag = tag.toLowerCase();
      if (!KEEP_TAGS_.test(tag)) return ' ';
      if (slash) return '</' + tag + '>';
      if (tag === 'a') {
        const href = (attrs.match(/\shref\s*=\s*("[^"]*"|'[^']*')/i) || [])[1];
        return href ? '<a href=' + href + '>' : '<a>';
      }
      if (tag === 'img') {
        const src = (attrs.match(/\ssrc\s*=\s*("[^"]*"|'[^']*')/i) || [])[1];
        if (!src) return '';
        const alt = (attrs.match(/\salt\s*=\s*("[^"]*"|'[^']*')/i) || [])[1] || '""';
        return '<img src=' + src + ' alt=' + alt + ' style="max-width:100%">';
      }
      return '<' + tag + '>';
    }
  );

  // メール冒頭のプレビュー用不可視文字(Substackが大量に入れる)を除去。
  // 生の文字と数値文字参照(&#847; 等)の両方の形で入っているため、両方消す。
  cleaned = cleaned.replace(/[\u034F\u00AD\u200B-\u200D\uFEFF\u2007]/g, '');
  cleaned = cleaned.replace(/&#(?:847|173|8199|820[3-5]|65279);/g, '');
  cleaned = cleaned.replace(/&#x(?:0*34f|0*ad|200[b-d]|feff|2007);/gi, '');

  // 不可視文字除去後に残る空白の連続を1つにまとめる(Kindleでの空白ページ化を防ぐ)
  cleaned = cleaned.replace(/(?:&nbsp;|[ \t]){3,}/g, ' ');

  return cleaned;
}

/** 差出人リストからGmail検索クエリを組み立てる。 */
function buildSearchQuery_() {
  const fromClause = CONFIG.SENDERS.map(function (s) {
    return '"' + s + '"';
  }).join(' OR ');
  return 'from:(' + fromClause + ') newer_than:' + CONFIG.SEARCH_WINDOW_DAYS + 'd';
}

/** Fromヘッダー(例: "Foo News <news@example.com>")が差出人リストに合致するか。 */
function isTargetSender_(fromHeader) {
  const lower = fromHeader.toLowerCase();
  return CONFIG.SENDERS.some(function (s) {
    return lower.indexOf(s.toLowerCase()) !== -1;
  });
}

/** 処理済みメールIDのマップ {messageId: 送信時刻(ms)} を読み込む。 */
function loadProcessedIds_() {
  const raw = PropertiesService.getScriptProperties().getProperty('processedIds');
  return raw ? JSON.parse(raw) : {};
}

/** 処理済みIDを保存する。保持期間を過ぎた古いエントリは削除する。 */
function saveProcessedIds_(processed) {
  const cutoff = Date.now() - CONFIG.PROCESSED_RETENTION_DAYS * 24 * 60 * 60 * 1000;
  const pruned = {};
  Object.keys(processed).forEach(function (id) {
    if (processed[id] >= cutoff) pruned[id] = processed[id];
  });
  PropertiesService.getScriptProperties().setProperty('processedIds', JSON.stringify(pruned));
}

/**
 * 本文がHTMLメールかどうかの判定。
 * プレーンテキストのメールでも getBody() は本文を返すため、
 * 実際にHTMLの構造タグが含まれているかで見分ける。
 */
function looksLikeHtml_(body) {
  return /<\s*(html|body|div|p|br|table|tr|td|img|span|a|blockquote|h[1-6])[\s>\/]/i.test(body);
}

/**
 * プレーンテキストをKindle向けHTMLに変換する。
 * 空行 → 段落区切り、行内の改行 → <br> として保持する。
 */
function plainTextToHtml_(text) {
  const escaped = escapeHtml_(text);
  return escaped
    .split(/\r?\n\s*\r?\n+/) // 空行で段落に分割
    .map(function (paragraph) {
      return '<p>' + paragraph.replace(/\r?\n/g, '<br>') + '</p>';
    })
    .join('\n');
}

function sanitizeFilename_(name) {
  return name.replace(/[\\\/:*?"<>|\r\n]+/g, ' ').trim().slice(0, 80) || 'newsletter';
}

function escapeHtml_(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// ─────────────────────────────────────────────
// 以下はセットアップ用の関数(手動で1回だけ実行する)
// ─────────────────────────────────────────────

/**
 * 定期実行トリガーを設定する。エディタから1回だけ実行すればよい。
 * 既存のトリガーがあれば作り直すので、間隔変更後の再実行も安全。
 */
function setupTrigger() {
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === 'main') ScriptApp.deleteTrigger(t);
  });
  ScriptApp.newTrigger('main')
    .timeBased()
    .everyMinutes(CONFIG.TRIGGER_INTERVAL_MINUTES)
    .create();
  console.log(CONFIG.TRIGGER_INTERVAL_MINUTES + '分ごとの自動実行トリガーを設定しました。');
}

/**
 * 動作テスト用: 差出人リストに合致する直近のメール1通を実際にKindleへ送信する。
 * 送信したメールは処理済みとして記録されるため、その後の自動実行で重複送信はされない。
 */
function testSendLatest() {
  const threads = GmailApp.search(buildSearchQuery_());
  for (let i = 0; i < threads.length; i++) {
    const messages = threads[i].getMessages();
    for (let j = messages.length - 1; j >= 0; j--) {
      const message = messages[j];
      if (isTargetSender_(message.getFrom())) {
        sendToKindle_(message);
        const processed = loadProcessedIds_();
        processed[message.getId()] = Date.now();
        saveProcessedIds_(processed);
        console.log('テスト送信しました: ' + message.getSubject());
        return;
      }
    }
  }
  console.warn('差出人リストに合致するメールが見つかりませんでした(検索範囲: 直近' + CONFIG.SEARCH_WINDOW_DAYS + 'd)。');
}

/**
 * 診断用: 差出人リストに合致する直近のメールについて、GASが取得できた本文の
 * 長さと末尾をログに出す。Kindleで本文が途中で切れるときの切り分けに使う。
 * 末尾にメルマガのフッター(配信停止リンク等)が見えていれば取得は完全で、
 * 切れているのはAmazonの変換側と判断できる。
 */
function debugLatestBody() {
  const threads = GmailApp.search(buildSearchQuery_());
  for (let i = 0; i < threads.length; i++) {
    const messages = threads[i].getMessages();
    for (let j = messages.length - 1; j >= 0; j--) {
      const message = messages[j];
      if (!isTargetSender_(message.getFrom())) continue;

      const html = message.getBody() || '';
      const plain = message.getPlainBody() || '';
      console.log('件名: ' + message.getSubject());
      console.log('getBody() の長さ: ' + html.length + ' 文字');
      console.log('getPlainBody() の長さ: ' + plain.length + ' 文字');
      console.log('--- getBody() 末尾500文字 ---');
      console.log(html.slice(-500));
      return;
    }
  }
  console.warn('差出人リストに合致するメールが見つかりませんでした。');
}

/** 処理済み記録をすべてリセットする(再送テストをしたいときのみ使用)。 */
function resetProcessedIds() {
  PropertiesService.getScriptProperties().deleteProperty('processedIds');
  console.log('処理済み記録をリセットしました。');
}

これを「Google Apps Script」に貼りつけて、あとは「Your Kindle Email」と「購読リスト」を書くだけ。解説はこちら

なお最近はメルマガが長過ぎた感じがあるので、本日はこれぐらいで終わります。あ、最後に1つだけ書かせて下さい🙇‍♂️


急がば回れ → 急がば休め。

昔なら「急がば回れ」が良かったけど、現代で必要なのは「急がば休め」だと思う。これを経験から書きます。

認知力が落ちると、気付かぬ内に崩れます。僕が鬱っぽくなった時期は、あとから見ると「悲惨」でした。

月収1000万円くらいの時にメンタルが崩れ、半年で「月収300万円」くらいに低下しました。オワコンな動きをしていた。

脳が回復してから当時の発信を見たら、それは「その場、その場の収益化」だけに動いていました。例えるなら、、

  • 種まきをして、花咲く前に収穫

こういった感じで、信頼を貯めては金で回収、そして繰り返し、という感じ。だから、大きなものが育ちません。

もし仮に、当時の僕が「なんか、メンタルがおかしいけど、まぁいいだろう、、ちょっと頭も痛いけど頑張ろう」と判断していたら、

もしかしたら現時点でも、同じような行動を続けていたかも。休まなかった人生を想像すると、本当に恐いです。


これは、全員で共通です。

脳が疲れ、認知力が落ちた状態で頑張っても、エネルギーは出ません。毎日30%のパワーで頑張る感じ。

僕は3年くらい低迷したし、たぶん多くの人からオワコンと思われたし、オンラインでもオフラインでも、多くの人が離れていった。

  • でも、全く問題ないと思ってます。
  • なぜなら、自転車もあるから。

これはDJ社長の言葉ですが、自分がどんなに頑張って走っていても、頭の良いやつはチャリに乗ると。だから考えろ、と。

思考力は人によってレベルが異なるけど、結局のところ、これから生きるのは自分の人生だし、だから自分なりに生きるべき。

  • 多くの人はインフルエンサーになれないし、億も稼げない。でも、その人なりの人生があり、そこを意識して生きるだけ。

疲れている脳は「あぁ、自分はインフルエンサーなんてなれないし、結局は頑張っても意味ないや。全て辞めよう」と考えます。

元気な脳は「今日も瞑想が気持ち良かった。メルマガも楽しいし、読者の方々に感謝しよう」と考えます。


という訳で、以上です。

情報を詰め込んだり、速読するのは、完全にAIの仕事です。だから任せつつ、自分は「人々の心」に着目する。

しかし「心」に着目するには、まずは自分の心を落ち着けないとだし、その「落ち着いた心」を通して、やっと世界が見える。

松下幸之助さん (パナソニック創業者) は、毎日仏壇の前で「世界を平等に見せてください」と祈っていたと言います。

30年くらい祈ったら、少し良くなったと、本人の書籍に書いていました。きっと、そういう事なのだと思います🧘

マナブ