MyASPで配信したメルマガをObsidianへ自動アーカイブする方法【Mac完全自動化】

ゆきの

MyASP × Gmail × Obsidian × launchd

配信予約したら、
勝手にアーカイブ

専用Gmailアドレスで受信→毎日22時+Mac起動時に自動でObsidianに保存。一度仕掛ければ、もう二度とコピペ作業はいりません。実装で実際にぶつかった問題の解決策も全部この中に。

📬
入力
専用Gmail受信箱

📝
出力
『5.4』件名.md

実行
22時 + 起動時

セットアップ
約60〜90分

なぜこの設計なのか

最初は「メインのGmailにフィルタを作って絞り込む」案でした。でも、メルマガ配信元から登録通知・コメント返信・Zoom申し込み等が同じFromアドレスで届いているので、「配信メルマガ本体だけを完璧に絞り込む」ことがほぼ不可能。だから方針転換。

解決策: メルマガ受信専用の新しいGmailアドレスを作って、そこで読者登録する。配信メルマガしか届かないので、フィルタが超シンプルに。これが最大のコツ。

仕組みの全体像

この仕掛けが裏で何をしているか。流れがわかると、どこかで止まったときに自分で原因を切り分けられます。

📤
MyASPで配信予約
メルマガ受信専用のGmailアドレスを読者リストに含めておく。

📥
配信時刻に専用Gmailへ届く
Gmailフィルタで自動的にラベル付け。

毎日22時 or Mac起動時に起動
launchdが自動でPythonスクリプトを実行。

🔍
imapclientでGmail接続
日本語ラベル名を正しく扱える専用ライブラリを使用。Message-IDで重複管理。

✏️
本文をmdファイル化
件名に既に『◯.◯』があれば重複付与しない。フロントマター付き。

🗂
年フォルダに自動振り分け
2026年/、2027年/…と自動で生成。iCloud同期でObsidianへ。

i
22時にMacがオフだったら? その回はスキップされますが、次にMacを起動した瞬間に自動的に追いつき処理されます(RunAtLoad: trueの効果)。Message-ID管理なので取りこぼし・二重保存は起きません。

10ステップで、仕掛け完了。

上から順に進めれば60〜90分で完成します。途中でつまずきやすいポイントは★印で警告しています。

1
メルマガ受信専用のGmailアドレスを用意
普段使いのGmailを汚さないために。

新規でGmailアドレスを作るか、既存の使っていないGmailアドレスを使う。

  1. そのアドレスでMyASPの読者登録を完了する
  2. メルマガが配信時刻にちゃんと届くことを確認(数分待って受信トレイをチェック)
これが最大のコツ: メインのGmailで頑張らない。「メルマガしか届かない箱」を作るのが圧倒的にラク。

2
2段階認証を有効化
アプリパスワード取得の前提条件。

専用Gmailにログインした状態で Googleアカウント → セキュリティ を開く。

  1. 「2段階認証プロセス」の状態を確認
  2. オフなら「オン」にする(SMS or 認証アプリ)
  3. 「オン」になっていれば次のステップへ

3
アプリパスワードを取得
Pythonスクリプトがログインするための鍵。

専用Gmailにログイン中の状態で アプリパスワード作成ページ を開く。

  1. 「アプリ名」に obsidian-archive と入力
  2. 「作成」をクリック
  3. 表示された16桁の文字列をすぐにメモアプリに保存
!
絶対に守ること:

・この16桁は他人に見せたらGmailを乗っ取られます

・チャット・SNS・メール・GitHubなどに絶対に貼らない

・もし漏洩したら、すぐにこのページで削除して再発行

・閉じると二度と表示されないので、必ずメモしてから閉じる

4
Gmailフィルタとラベルを作る
配信メルマガだけにラベルを付ける。

専用Gmailを開いて、右上の歯車⚙️ → すべての設定を表示 → フィルタとブロック中のアドレス → 新しいフィルタを作成

  1. From欄: MyASPの配信元アドレスを入力(例: info@example.jp)
  2. 件名欄: (全角左二重カギ括弧)
  3. 「フィルタを作成」をクリック
  4. 「ラベルを付ける」にチェック → 「新しいラベル」 → 任意の名前(例: Obsidianへのメルマガアーカイブ)
  5. 「○件の一致するスレッドにもフィルタを適用する」にチェック
  6. 「フィルタを作成」で完了
!
必ず控えておく: ラベル名は大文字小文字も含めて完全一致でconfig.jsonに書く必要があります。サイドバーで実際の表示名をしっかり確認してください。

5
作業フォルダを作って仮想環境を構築
Macのターミナル作業の出発点。

ターミナルを開く(+Spaceで「ターミナル」検索)。

Terminal

mkdir -p ~/myasp-archive
cd ~/myasp-archive
python3 -m venv venv
source venv/bin/activate
pip install imapclient

5行を順に実行(まとめて貼り付けてもOK)。

なぜvenv? HomebrewでPythonを入れていると、システム直下にライブラリを入れようとすると externally-managed-environment エラーが出ます。仮想環境なら安全に入れられます。

プロンプトの先頭に (venv) が付き、Successfully installed imapclient が表示されればOK。

6
Obsidianの保存先パスを取得
FinderからD&Dで正確に。

Finderでメルマガを保存したい親フォルダ(年フォルダの1つ上)を1回クリックして選択。

そのフォルダをターミナルウィンドウへドラッグ&ドロップ。長いパスが自動入力される。

/Users/[ユーザー名]/Library/Mobile Documents/iCloud~md~obsidian/Documents/[Vault名]/[途中のパス]/メルマガ

そのパスをマウスでドラッグしてコピー(+C)→ メモアプリに保存。

ターミナルでは Control+U で入力した文字を全部消す。

!
D&Dで入力したパスには\\(バックスラッシュ)でスペースがエスケープされていますが、JSON設定では不要なので外す。例: Mobile\ DocumentsMobile Documents

7
設定ファイル(config.json)を作る
アドレス・パスワード・ラベル・保存先をまとめる。

Terminal

nano config.json

下を貼り付けて、4か所を自分の値に書き換える。

config.json

{
  "gmail_address": "専用アドレス@gmail.com",
  "app_password": "取得した16桁スペースなし",
  "label": "Obsidianへのメルマガアーカイブ",
  "vault_base_path": "/Users/[ユーザー名]/Library/Mobile Documents/iCloud~md~obsidian/Documents/[Vault名]/[途中]/メルマガ"
}

  1. Control+OEnterControl+X で保存

動作確認(中身は表示されません):

確認コマンド

python3 -c "import json; c=json.load(open('config.json')); print('gmail_address:', c['gmail_address']); print('app_password length:', len(c['app_password'])); print('label:', c['label']); print('vault_base_path:', c['vault_base_path'])"

app_password length: 16 が表示されればOK。違う数字ならスペースが混ざっている可能性。再度nano config.jsonでスペースを削除。

8
アーカイブスクリプト(archive.py)を作る
日本語ラベル対応・重複防止・年フォルダ自動振り分け。

Terminal

nano archive.py

下のコードをそのまま全部貼り付け(書き換え不要)。

archive.py

import email, json, re
from email.header import decode_header
from email.utils import parsedate_to_datetime
from datetime import datetime
from pathlib import Path
from imapclient import IMAPClient

BASE = Path(__file__).resolve().parent
CONFIG = json.loads((BASE / "config.json").read_text("utf-8"))
SEEN_FILE = BASE / "seen.txt"
LOG_FILE = BASE / "archive.log"


def log(msg):
    line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}\n"
    LOG_FILE.open("a", encoding="utf-8").write(line)


def load_seen():
    if not SEEN_FILE.exists():
        return set()
    return set(SEEN_FILE.read_text("utf-8").splitlines())


def add_seen(uid):
    SEEN_FILE.open("a", encoding="utf-8").write(uid + "\n")


def decode(s):
    if s is None:
        return ""
    parts = decode_header(s)
    return "".join(
        (b.decode(enc or "utf-8", errors="replace") if isinstance(b, bytes) else b)
        for b, enc in parts
    )


def extract_body(msg):
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                payload = part.get_payload(decode=True) or b""
                charset = part.get_content_charset() or "utf-8"
                return payload.decode(charset, errors="replace")
        return ""
    payload = msg.get_payload(decode=True) or b""
    charset = msg.get_content_charset() or "utf-8"
    return payload.decode(charset, errors="replace")


def safe_filename(name, maxlen=80):
    name = re.sub(r'[\\/:*?"<>|]', "", name)
    name = name.strip()
    return name[:maxlen] if len(name) > maxlen else name


def main():
    seen = load_seen()
    new_count = 0

    with IMAPClient("imap.gmail.com", ssl=True) as M:
        M.login(CONFIG["gmail_address"], CONFIG["app_password"])
        M.select_folder(CONFIG["label"])

        ids = M.search(["ALL"])
        if not ids:
            log("no messages found")
            return

        msgs = M.fetch(ids, ["RFC822"])

        for uid, data in msgs.items():
            raw = data[b"RFC822"]
            msg = email.message_from_bytes(raw)
            msg_id = msg.get("Message-ID", "").strip()
            if not msg_id or msg_id in seen:
                continue

            subject = decode(msg.get("Subject", ""))
            date_hdr = msg.get("Date", "")
            try:
                dt = parsedate_to_datetime(date_hdr)
            except Exception:
                dt = datetime.now()

            year_folder = Path(CONFIG["vault_base_path"]) / f"{dt.year}年"
            year_folder.mkdir(parents=True, exist_ok=True)

            # 件名にすでに『◯.◯』形式の日付があれば追加付与しない
            date_label = f"『{dt.month}.{dt.day}』"
            if re.search(r"『\d+\.\d+』", subject):
                base_name = subject
            else:
                base_name = f"{date_label}{subject}"
            filename = safe_filename(base_name) + ".md"
            out_path = year_folder / filename

            if out_path.exists():
                stem = out_path.stem
                n = 2
                while (year_folder / f"{stem} ({n}).md").exists():
                    n += 1
                out_path = year_folder / f"{stem} ({n}).md"

            body = extract_body(msg)
            front = (
                f"---\n"
                f"date: {dt.isoformat(timespec='minutes')}\n"
                f"subject: {subject}\n"
                f"from: {decode(msg.get('From',''))}\n"
                f"---\n\n"
            )
            out_path.write_text(front + body, encoding="utf-8")
            add_seen(msg_id)
            seen.add(msg_id)
            new_count += 1
            log(f"saved: {dt.year}年/{out_path.name}")

    log(f"done: {new_count} new")


if __name__ == "__main__":
    main()

  1. Control+OEnterControl+X で保存
imapclient を使う理由: Pythonの標準imaplibは日本語ラベル名でエラーを吐きます(UnicodeEncodeErrorまたはCould not parse command)。imapclientはGmailの日本語ラベルをUTF-7に正しく変換してくれます。

9
手動で動作確認
launchd登録の前に必ずここをクリア。

Terminal

python3 archive.py
cat archive.log

ログに saved: 2026年/『5.6』〜.mddone: N new が出ればOK。Obsidianを開いて、年フォルダにファイルが追加されているか確認。

!
エラーが出たら: 後述の「つまずいたら」セクションを参照。多くの場合、ラベル名の大文字小文字違い、アプリパスワードのスペース混入、JSONの構文エラーが原因。

10
launchdで自動化
22時 + Mac起動時の2タイミングで実行。

Terminal

nano ~/Library/LaunchAgents/com.local.myasp-archive.plist

下を貼り付けて、[ユーザー名]を3か所書き換える(whoamiコマンドで確認可)。

com.local.myasp-archive.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.local.myasp-archive</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/[ユーザー名]/myasp-archive/venv/bin/python3</string>
    <string>/Users/[ユーザー名]/myasp-archive/archive.py</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Hour</key>
    <integer>22</integer>
    <key>Minute</key>
    <integer>0</integer>
  </dict>
  <key>RunAtLoad</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/Users/[ユーザー名]/myasp-archive/stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/[ユーザー名]/myasp-archive/stderr.log</string>
</dict>
</plist>

2つの重要ポイント:

1. /usr/bin/python3 ではなくvenvのpython3を指定(imapclientが使える環境)

2. RunAtLoad: trueMac起動時にも自動実行(22時を逃しても取りこぼし回収)

  1. Control+OEnterControl+X で保存

登録して動作確認:

Terminal

launchctl load ~/Library/LaunchAgents/com.local.myasp-archive.plist
launchctl list | grep myasp

– 0 com.local.myasp-archive が表示されればOK。load直後にも1回スクリプトが実行される(RunAtLoad: trueの効果)ので、cat archive.logで確認可能。

つまずいたら、ここを。

実装時に実際に発生したエラーと、その解決策。

Err 01

UnicodeEncodeError: ‘ascii’ codec…

原因

標準imaplibは日本語ラベル名を扱えない。

対処

imapclientを使う(本マニュアル通りに作っていれば発生しない)。

Err 02

SELECT command error: BAD

原因

ラベル名が見つからない、または書式不一致。

対処

下記コマンドで全ラベル名を確認し、config.jsonに完全一致(大文字小文字含む)で書く:

確認

python3 -c "from imapclient import IMAPClient; import json; c=json.load(open('config.json')); M=IMAPClient('imap.gmail.com', ssl=True); M.login(c['gmail_address'], c['app_password']); [print(repr(f[2])) for f in M.list_folders()]; M.logout()"

Err 03

externally-managed-environment

原因

HomebrewのPythonにシステム直下でpip installしようとした。

対処

必ずvenvを作って、その中でpip install。Step5の手順通りに。

Err 04

app_password length が16以外

原因

16桁にスペースが含まれている、または余計な文字が入っている。

対処

nano config.jsonで開き、app_password行のスペースをすべて削除。16文字きっかりに。

Err 05

JSONDecodeError

原因

config.jsonの構文ミス(カンマ抜け、ダブルクオート抜け、{}重複等)。

対処

cat config.jsonで中身を見て、Step7のテンプレと比較。心配ならファイルを削除して作り直し。

Err 06

launchdが22時に動かない

原因

22時にMacがスリープ・電源オフだった。

対処

RunAtLoad: trueを入れていれば、次のMac起動時に自動実行されて取りこぼし回収。手動なら cd ~/myasp-archive && source venv/bin/activate && python3 archive.py

Err 07

登録通知などが混じる

原因

Gmailフィルタの絞り込みが甘い。

対処

件名の条件を に絞ると配信メルマガのみに。それでも混じる場合はObsidianで手動削除。

Err 08

『5.6』が件名に二重に付く

原因

登録通知などは件名にすでに『◯.◯』が含まれている。

対処

本マニュアルのarchive.pyは件名に『◯.◯』があれば追加付与しない仕様。古いコードを使っているなら最新版に書き換え。

もしアプリパスワードが漏れたら

チャットや投稿に誤って貼ってしまった場合の緊急対応。

!
すぐに無効化:
アプリパスワード管理ページ で該当エントリを削除。これで漏れた16桁は即時無効。その後、新規発行してconfig.jsonを更新。

停止・再開・手動実行

必要なときに見るコマンド集。

一時停止

launchctl unload ~/Library/LaunchAgents/com.local.myasp-archive.plist

再開

launchctl load ~/Library/LaunchAgents/com.local.myasp-archive.plist

手動で1回だけ実行

cd ~/myasp-archive
source venv/bin/activate
python3 archive.py

動作ログを見る

tail -n 30 ~/myasp-archive/archive.log

エラーログを見る

cat ~/myasp-archive/stderr.log

時刻を変える(例:21時に変更)

nano ~/Library/LaunchAgents/com.local.myasp-archive.plist
# <integer>22</integer> を <integer>21</integer> に書き換え、保存
launchctl unload ~/Library/LaunchAgents/com.local.myasp-archive.plist
launchctl load ~/Library/LaunchAgents/com.local.myasp-archive.plist

ABOUT ME
ゆきの
ゆきの
守ることから始めた経営者
ゆきの(1981.7.19 三重県出身 A型 蟹座) 法人会社(物販)1社経営で8期(年商1.2億円) 2021年12月からセルフエステサロン運営。 2023年から法人経営起業サポートコミュニティ[YES]を開講中!(生徒数40名、内女性30名) 2024年1月から個別相談を開始し、2025年現在では平均月40社のサポート中。 2024年夏から一般企業の経理担当を開始し、 融通の効かない大手税理士事務所からfreee会計への移行サポートや、銀行融資が獲得しやすい決算書の作成をサポートさせていただいています。 情報発信での収益は年間1,000万円超。
記事URLをコピーしました