Microsoft 365 / Google Workspace の代わりを、自前で持つ。握られず、現場で動かす。本書は v2 の未決事項を決定し、実装に入れる形にしたもの。
v3.1 での追記(実装着手前のレビューで埋めた三つの穴):
- traversal(経路の権限判定)を決定:権限判定は当該ディレクトリの xattr だけで行い、祖先は見ない(5 章)。帰結として、一覧の規則(r を持たない子ディレクトリは名前ごと見えない)と、入口一覧の必要性を明記。
- ブートストラップと信頼モデルを決定:権限は三層(OS root・PocketBase スーパーユーザー・Workspace 管理者)に分け、混ぜない。初期化はサーバー上の CLI 一発(6 章末尾・7 章)。
- トークン検証の標準実装を決定:PocketBase への introspection + 短 TTL キャッシュ。差し替え式 I/F の型を固定(4.4)。
v2 からの主な変更:
-
名称を決定:日本語名「蔵」、英語表記 aiseed workspace。識別子は常に aiseed との結合で使う(ドメイン・リポジトリは aiseed 配下)。
-
データルートを決定:
/srv/workspace/。 -
共通 ID を決定:内部の正は独自識別子(PocketBase のレコード ID)。メールは認証・招待・表示のための属性。
-
台をまたぐ認証を決定:連携の口が持つのは「トークン検証関数」一つ。実装を台ごとの設定で差し替え可能にする。フェデレーションは作らない。
-
カレンダーを決定:内輪共有(ファイル + API)を正とし、ICS 購読配信で既存カレンダーへ届ける。保存形式は .ics。Radicale(CalDAV)は需要が証明されたら後載せ。
-
ONLYOFFICE Docs 9.4 の変化を反映:同時接続上限の撤廃、単一プロセス化、RabbitMQ・DB 依存の除去。
-
Flet のバージョン方針を追記:開発は最新安定版(0.85.3)に追随(移行ガイドで追従)、配布物はテスト済みバージョンに固定。書き方は新方式=宣言的スタイル(Flet Component API、@ft.component + フック)で統一。
-
サーバー OS とファイルシステムを確定:OS は Debian、ファイルシステムは標準の ext4 のまま(既定の xattr 上限で対象規模に桁で足りる。退路は
tune2fs -O ea_inode一コマンド)。
組織やグループで、ファイル・文書・カレンダーを共有する。いま Microsoft 365 / Google Workspace でやっていること。ただし——
- データは自分で持つ。
- 高額な費用を払わない。
- 他のシステムとも共有できる(標準を話す)。
- 全部が一つの世界としてつながる(Workspace としての一体感)。
対象は、中小企業やグループの真剣な業務利用。当面の設計目標は 1 台あたり 〜100 人。 それを超える需要には一台のスケールアップでなく台を分けて応える(現場分散、判断軸 3)。 規模に依存する決定は、単一の正解を押し付けず選択肢を併記し、台ごとの事情で選べる形にする (例:リバースプロキシ、メール)。
決定はすべて、この優先順位から導く。迷ったら、ここへ戻る。
- 配って現場で動く渡しやすさを最優先する。各現場に渡して、その手元で動き続けるもの。
- 握られない・自前ホスト。
- 現場分散。中心に集約せず、台ごとに完結。
- 最小依存・最小抽象。ファイル管理はファイルシステムで、認証は検証済みを借りる。自前の抽象を増やさない。
- 書かれた汎用は借り、書かれていない固有だけ書く。機能は大きく、コードは小さい。
- またぐものはつなぐ、完結するものは現場で。
- 既知は構造を書いてから、未知は使いながら育てる。
需要は今ある。Microsoft 365 / Google Workspace の代わりを欲しい人は多い。だから、二つを並行させる。
- いま(需要に応える・観察する):DocSpace を運営・配布する。完成品なので、立てれば文書・共同編集・共有が今すぐ動く。乗り換えに今すぐ応え、現場が本当に何を要るかを観察する場にする。
- 育てる(行き先):自前 Workspace を作る。DocSpace では届かない部分——業務システムとの一体化、ファイルベースの権限、握り——を、観察を糧に育てる。
DocSpace は入口であり参照実装。最終形ではない。DocSpace を動かすことが、自前 Workspace の設計を直す観察になる。
役割を、借りる・書く・つなぐ、で分ける。
office 文書(docx/xlsx/pptx)の描画と共同編集の同期。書かれた汎用の極北。書き直さない。完成品として隣に置くのではなく、編集の部品として組み込む。
9.4(2026 年 5 月)で、状況がこちらに近づいた:
- Community Edition の同時 20 接続の上限が撤廃された。規模の制約が消えた。
- アーキテクチャが単一プロセスに統合され、RabbitMQ と DB への依存が除去された。軽くなり、「配って現場で動く」に直接効く。
- ライセンスは AGPL v3 のまま、帰属表示・著作権表示・改変版の明示の要件が追加された。配布物に同梱するなら、表示要件を満たすこと(配り方・9 章の確認項目)。
連携で自前実装になるのは三点。(1) JWT 秘密鍵の設定と検証、(2) 保存コールバック(編集終了時に Docs サーバーが叩く URL の実装)、(3) 共同編集セッションを束ねる document key の管理。いずれも Workspace 層(Python)の仕事。
ここが設計の中核。
- ファイルの実体は、Linux のファイルシステムに置く(PocketBase や DB には入れない)。
- 全ファイルは単一のサービスアカウントが所有する(Web アプリは単一 UID で動くため)。実際の owner/group/mode フィールドは権限の正にならない。
- 権限の正は、各ディレクトリの xattr(拡張属性、user. 名前空間)* に置く。グループごとの権限を xattr として記録し、アプリ(Python/Rust)が読んで enforce する。
- 基本パーミッションでは一つのディレクトリにグループを一つしか付けられず、「グループ A は閲覧、グループ B は編集」が表せない。xattr なら複数グループへの割り当てを一つのディレクトリに持てる。これが xattr を選ぶ実装上の理由。
- グループの定義と所属も、ファイル上に持つ(ルート直下の groups ファイル)。/etc/group は使わない——OS アカウントと Workspace 利用者を絡めない。
- 権限のためのデータベースは、持たない。権限判定 = xattr とグループファイルへの問い合わせ。DB とファイルシステムの二重持ち・同期が、消える。
整合性の要点:正しさの要は性能ではなく同時更新の原子性。xattr の更新、グループファイルの更新は、flock と atomic rename(一時ファイルに書いて rename)で守る。Rust の出番があるとすれば、まずここ。
運用の要点:xattr は標準的なコピー・バックアップで落ちやすい。rsync は -X、tar は --xattrs が必要。バックアップ・移設の手順とスクリプトを配布物に同梱する。
- 認証(誰であるか、ログイン)は、PocketBase で標準化する。検証済みを借り、自作しない(認証は書かれた汎用の極北で、間違えると最も危ない)。
- PocketBase の役割は認証専任:利用者の登録・ログイン・トークン発行。
- PocketBase は内部にデータを持つが、それは認証と利用者プロフィール(表示名・メール)のため。ファイルと権限のデータは持たせない(4.2 と混ぜない)。
- 連携の口は Python API サーバー(FastAPI 等)。ファイル・共有・カレンダーの API を提供する。フロントも外部システムも、同じ口を通る。
- 口の実装の選択肢(規模依存でなく依存量依存。口は薄く保ち、いつでも移れることが保険):A. FastAPI(現行)——自動 OpenAPI が外部連携の仕様書を無償でくれる。代償は依存の塔(pydantic 系の破壊的変更の前歴)と async の習熟。配布物は版固定。B. Flask + WSGI——同期で読みやすく依存が小さい、10 年級に枯れた中道。C. 標準ライブラリのみ(http.server)——依存ゼロ、〜100 人・LAN なら性能も足りる。ロジックは全てコアエンジンにあり、口は数百行なので、乗り換えはどの方向へも小さい。実害(追従の苦痛)が出てから移る。
- API が認証について持つのは、トークン検証関数一つ。標準実装は PocketBase トークンの検証。台ごとの設定で実装を差し替えられる(既存の社内認証・OIDC を使いたい台は、検証実装を差し替える)。権限判定(xattr + グループ)は検証の後ろにあり、認証方式が何であっても変わらない。
- 検証関数の型(v3.1 で固定):
トークン文字列 → { user_id, display_name, email } | 検証失敗。ID だけでなく表示名・メールまで返す型にすることで、「ID → 表示名の解決」(6 章の一層分の代償)がこの関数に畳まれ、差し替え先でも同じ型で済む。 - 標準実装の方式(v3.1 で決定):PocketBase への introspection(auth-refresh 相当の API 問い合わせ)+ 短 TTL キャッシュ(30〜60 秒)。PB のトークンは署名鍵が利用者レコードごと(tokenKey)で、公開鍵によるローカル検証ができないため、選択肢は (A) 毎回 introspection、(B) A + キャッシュ、(C) PB の SQLite を直接読んでローカル検証、(D) ログイン時だけ PB を使い自前セッションを発行、の四つ。C は PB 内部実装への依存(判断軸「検証済みを借りる」に反する)、D は認証の一部自作になるため退け、B を標準とする。同一ホスト構成では往復は localhost で実質ゼロ、失効(パスワード変更による tokenKey 回転)の遅れは TTL 分のみ。D は複数の認証バックエンドを本気で抱える台が現れたときの発展形として退路に置く。
- Flet と API の関係:権限判定・ファイル操作は Python ライブラリ(コアエンジン)として実装し、API はその一つの口。Flet(サーバー側 Python)は同じライブラリを直接 import してよいが、認可の判定は必ずコアエンジンを通る——口が二つでも、判定は一つ。
- 口とバッチの分離(v3.1 追記):口は有界の軽い仕事だけ(認証 → 権限判定 → バイト列の出し入れ)。重い・長い処理——メールの IMAP 取り込み、迷惑メールの LLM 判定、索引作り、バックアップ——は別プロセスの働き手とし、systemd タイマー / cron で起動してコアエンジンを直接 import する。async サーバーに重い同期処理を入れると全利用者が止まり、デプロイと障害が相互に巻き添えになるため。キュー基盤(Celery・Redis 等)は持ち込まない——受け渡しはファイルシステムと atomic rename で足りる。ファイルベースという中核設計が、そのままジョブキューの代わりになる。
- フェデレーションは作らない。台をまたぐ共有は、招待(メールで相手台にアカウントを作る)か、共有リンク(トークン付き URL)の二経路に畳む。各台は完全に独立——「現場分散・台ごとに完結」。
- 利用者の画面(一覧・表示・操作・共有・招待・カレンダー)は Flet で書く。自己完結型の Widget。
- Flet は Python がサーバー側で動くサーバー駆動 UI。選ぶ理由は「フロントもロジックも Python 一言語で、層を増やさず書ける単純さ」。現場の LAN・小規模利用なら、セッションごとのサーバー資源は問題にならない。
- クライアント側で完結する独立したフロントが要件になったら、Flutter + 4.4 の API に差し替えられる。API の口を先に立てておくのは、この退路のためでもある。
- もう一つの退路:サーバーレンダリングの素の Web(FastAPI/Flask + Jinja2 テンプレート + 最小限の素の JS)。Flet の追従が苦痛になったらこちらへ。蔵の画面(一覧・フォーム・予定)の範囲なら「フレームワーク」でなく数百行のテンプレートで済み、バージョン追従が消える。なお Flet 自体を作り直す選択は採らない——UI フレームワーク自作は「書かれた汎用は借りる」に反する別事業。Flet は OSS なので、最悪でも版固定・fork という退路が常にある(クラウドと違い OSS の依存は握られていない)。
- 固有のロジックは Python。同時更新の原子性・大量アクセスの権限判定は、必要になってから Rust。
- Flet の書き方は新方式=宣言的(declarative)スタイルで統一:@ft.component デコレータ+ use_state() 等のフック+ Observable な状態クラスで、状態から UI を返す書き方(公式名は Flet Component API / Flet Components)。旧方式=命令的(imperative)スタイル(コントロールを直接書き換えて page.update() を呼ぶ)は使わず、混在させない。コンポーネントが状態を内に持つ更新の単位=自己完結型 Widget の方針と一致する。外部の状態管理層は足さない。
- Flet のバージョン方針:Flet は 1.0(安定版)に向けて変更が多い時期にある(2026 年 6 月時点の最新安定版は 0.85.3、破壊的変更の移行ガイドがリリースごとに公開される)。開発では常に最新の安定版に追随し、リリースごとに移行ガイドで追従する。dev プレリリースは使わない。Component API は Flet の中で最も新しく動いている部分(ft.Router・ft.use_dialog() は 0.85 で追加)なので、追随はとくにここに効く。配布物だけは、テスト済みバージョンに固定して同梱する——開発は追随、現場は固定。
前提の事実:個人は既にカレンダーを持っている(Google カレンダー、iPhone のカレンダー)。Workspace の仕事は個人のカレンダーになることではなく、グループの予定を持ち、各自の既存カレンダーに流し込むこと。
- カレンダー=ディレクトリ、イベント=その中の .ics ファイル。権限は xattr モデルそのまま(閲覧/編集/管理)。カレンダーのためだけの仕組みを増やさない——「全部が一つの世界」。
- 書き込みは Flet のフロントから。グループの予定を入れるのは少数、見るのは全員。
- 配信は ICS 購読(読み取り専用フィードの URL)。API が .ics ファイル群からフィードを生成して返す。Google カレンダー・Apple カレンダー・Outlook のすべてが標準機能で購読できる。クライアントに何も入れさせない。
- フィードの認可(v3.1 で追加):購読クライアントはヘッダ認証ができないため、認可は URL 内トークンが担う。仕組みは共有リンク(対象=カレンダーのディレクトリ、権限 r)にそのまま畳む——取り消し・期限も共有リンクの機構で効く。カレンダー専用の認可は作らない。
- CalDAV サーバー(Radicale)は初手では立てない。CalDAV の強み(既存クライアントからの双方向読み書き)は、主流の Google カレンダーが CalDAV クライアントとして外部に繋がらないため、ほぼ働かない。認証の二重化・rights の二重表現・DAVx⁵ の導入手順という費用だけが残る。
- 退路は保存形式が担保する。Radicale のストレージはディスク上の .ics ファイルそのもの。保存形式を最初から .ics にしておけば、双方向の需要が現場から本当に出たとき、同じデータの上に Radicale を後から載せられる。CalDAV を捨てるのではなく、需要が証明されるまで遅延させる。
- 既知の弱点:ICS 購読の更新頻度は購読側が決め、特に Google カレンダーは反映が遅い(数時間〜、制御不能)。リアルタイムの正は Workspace のフロントにあり、既存カレンダーへの配信は「漏らさず把握するため」と位置づける。即時性がこれで足りるかは、観察項目。
- 参照実装:Workspace に何が要るか(ルーム・共有・権限・ユーザー・文書連携)を、動いている完成品から読む。
- 権限の差し替えは、期待しない:DocSpace のルーム権限は内部の DB が持ち、外から差し替えられない。門番方式でも「誰が到達できるか」の粗い制御まで。
- DocSpace は需要対応と観察の場・機能の参照に徹する。自前 Workspace は、ファイルベースという別の設計思想に立つ、別の lean な実装。
- 権限はグループに付け、人はグループに出入りする(個人に直接貼らない)。
- 権限はディレクトリに付け、ファイルは入れ物の権限を受ける。
- ディレクトリは入れ子。各ディレクトリが、グループごとの権限を独立に持つ(xattr)。
- 権限ビットは三つ、自前で定義する:
- 閲覧(r):一覧でき、読める・ダウンロードできる。
- 編集(w):書ける・作れる・消せる・アップロードできる。
- 管理(a):配下に子ディレクトリを作り、グループと権限を割り当てられる。共有・招待を出せる。
- Linux の rwx をそのまま借りない理由:Linux のディレクトリの x は通過のみで、作成・削除は w。借用すると PC の権限の知識がかえって誤解を生む。倣うが、同一ではない。
- 継承はしない。子の作成時に親の権限をコピーし、管理者が変更できる。
- traversal(v3.1 で決定):権限判定は当該ディレクトリの xattr だけで行い、祖先は見ない。
/a/bに r を持つ人は/aに何も持たなくても/a/bに到達できる。祖先への権限要求は「このフォルダだけ経理グループに」を祖先の権限変更に波及させ、管理(a)の現場完結性と衝突するため採らない。継承なし・ディレクトリ独立の思想と一貫させる。 - 一覧の規則(traversal の帰結 1):ディレクトリに r を持つ人は、その中のファイルはすべて見える(ファイルは入れ物の権限を受ける)。しかし中の子ディレクトリはそれぞれ自分の xattr を持つので、r を持たない子ディレクトリは名前ごと見えない(名前自体が秘密になり得る)。
- 入口一覧(traversal の帰結 2):ルートから辿れない場所に権限を持つ人が出るため、フロントに「自分が見える場所の一覧(共有ドライブ型の入口)」が必須になる。実装はグループ → 到達可能ディレクトリの逆引き。小規模なら全ディレクトリ xattr の走査(起動時 + 更新時追従、またはオンデマンド)で足りる。性能が問題になったら索引を足すが、索引は導出物であり権限の正は常に xattr。
- 管理(a)を持つ人=そのディレクトリの管理者。配下を自分で設計できる=管理者が現場で完結して動ける。
- 共有リンク(組織外への、トークン付き URL での共有):リンク=トークン+対象+権限(閲覧/編集)+期限、をファイル上に持ち、アプリが enforce する。
- xattr・グループファイル・共有リンク・.ics に書く利用者 ID は、PocketBase のレコード ID(不変・自動発行・PII を含まない)。
- メールアドレス・表示名は PocketBase 側の属性。変わってよい——権限データには触らない。
- 表示には ID → 表示名の解決が常に要る。これは「権限データは書き換えない、人の属性は変わってよい」という分離のために払う、一層分の代償。
- 退会時は PocketBase 側の属性を消せば、権限データに PII は残らない。
- 台 A の利用者と台 B の利用者を結ぶのは、招待時のメールアドレス。内部 ID は台ごとに独立でよい。
権限は三層あり、混ぜない。「OS アカウントと Workspace 利用者を絡めない」をブートストラップまで延長する。
| 層 | 何者か | 役割 |
|---|---|---|
| OS root | サーバーの管理者(sudo) | インストール時だけ使う。サービスアカウント作成、/srv/workspace/ 作成、systemd 登録 |
| PocketBase スーパーユーザー | 認証システムの運用資格 | PB の管理 UI に入る鍵。Workspace の利用者ではない。DB 管理者に相当 |
| Workspace 管理者 | PocketBase の普通の利用者 | g_admin グループに属し、ルートの xattr {"g_admin":"rwa"} で効く |
信頼モデル:OS root はディスク上のすべて(xattr・groups・PB の SQLite)を読み書きできる。これは防げないし、防ぐ対象でもない——「データは自分で持つ」という思想上、箱の root を持つ者=データの主である。Workspace の権限モデルが守るのは利用者間の境界であって、サーバー運用者からの秘匿ではない。
ブートストラップ(CLI 一発):
- インストール(OS root で実行):サービスアカウント
workspaceを作成し、/srv/workspace/をその所有・mode 0700 で作り、PB・API・フロントを systemd サービス(User=workspace)として登録。OS root の出番はここで終わり。 - 初期化(サーバー上で CLI 一発):
kura initが、(a) PB スーパーユーザーを生成(資格情報を表示、運用者が保管)、(b) 最初の Workspace 利用者を PB に作成、(c)groupsファイルをg_admin(メンバー=その利用者)で初期化、(d) ルートの xattr に{"g_admin":"rwa"}を設定。 - 以降はすべて Web から。最初の管理者がルートに a を持つので、トップレベルの設計・グループ作成・招待は現場で完結する。
CLI を選ぶ理由:サーバーに入れる人(=運用者)だけが実行でき、Web に「未初期化状態の窓」を晒さない。スクリプト化できるので配り方 A・C にそのまま乗る。配り方 B には、CLI が初回ログイン用 URL を印字する形で渡す。
実装開始に要る最小の取り決め。組みながら直してよい(未知=使いながら育てる)。
- ディレクトリ構成:データルートは
/srv/workspace/。その配下が共有ツリー。ルート直下にgroups(グループ定義・所属)、links(共有リンク)。 - xattr スキーマ:二つだけ。
user.ws.perm(ディレクトリのみ)に、グループ ID → 権限ビットの対応を JSON で持つ(例{"g_abc":"rwa","g_def":"r"})——権限の正。キーを一つにするのは、更新の原子性を xattr 一回の書き込みに畳むため。user.ws.creator(ファイル・ディレクトリ両方)に作成者の利用者 ID を持つ——表示・記録用のメタデータであり、権限判定の入力ではない(作成者の暗黙の権利は作らない。権限の入力はグループのみ)。実 FS の所有者はサービスアカウントなので、作成者はここに記録しない限り失われる。タイムスタンプは FS の mtime をそのまま使い、自前で持たない。 - groups ファイル:JSON(グループ ID、表示名、メンバーの利用者 ID 列)。更新は flock + atomic rename。flock はデータファイル自身でなく、別のロックファイル(例
groups.lock)に掛ける——rename はデータファイルの inode を差し替えるため、データファイルへのロックは新旧の取り違えを生む。 - パス安全(v3.1 で追加):API が受けるパスは、データルート配下への解決を強制する。
..を含むパスは拒否、シンボリックリンクは作らせない・辿らない、ファイル名は NFC に正規化して保存(日本語名の NFC/NFD 揺れを吸収)。groups・linksなどルート直下の管理ファイル名は予約語として利用者のパスから除外する。 - 共有リンク(v3.1 で追加):
linksファイルにはトークンのハッシュを保存する(平文を置かない)。対象は当面パスで参照し、移動・改名でリンクは切れると割り切る(観察項目。需要が出たら安定 ID を検討)。 - サーバー OS とファイルシステム:OS は Debian、ファイルシステムは標準の ext4 のまま。特別なフォーマットオプションもパーティション要件も置かない。根拠:ext4 既定の xattr 上限(全 xattr 合計 ≒ 4KB)で、1 ディレクトリあたり約 150 グループまで持て、小規模組織・現場という対象では一つのディレクトリに付くグループは数個〜十数個であり、桁で余裕がある。上限超過(ENOSPC)は setxattr が失敗するだけで中途半端な状態は生まれず、アプリは「このフォルダに設定できるグループ数の上限」という明確なエラーで断る。万一上限に近づく運用が現れたら、
tune2fs -O ea_inodeを既存パーティションに後から適用すれば再フォーマットなしで一値 64KB に広がる——退路は一コマンド。Debian 標準そのまま・配布スクリプトにファイルシステムの指定なし、が「配って現場で動く」「最小依存」に最も合う。 - 検証項目(実装順序 2 で実機確認):ext4 での xattr 実効上限の実測(perm + creator 同居で何グループまで入るか)と、超過時(ENOSPC)のエラー挙動。rsync -X / tar --xattrs での保全。flock の競合挙動。検証は
kura fscheckとして配布物に同梱し、現場のファイルシステムでも実行できる。 - 実測結果(2026-06、ext4 既定):perm + creator 同居で 212 グループ(グループ ID
g_+ 16 進 8 桁、ビット rwa の場合)。試算の「約 150」を上回り、対象規模に桁で足りる。超過(ENOSPC)は原子的に失敗し、直前の値は無傷で残る。tar --xattrs の往復で user.* xattr は保全される。flock の競合は並行 4 スレッド × 200 更新で欠落なし。
- 認証(PocketBase)。ログイン、トークン発行、最初の管理者の作り方。
- ファイルと権限(ファイルシステム + xattr)。7 の初期案で共有を立て、原子性・xattr バックアップ・サイズ上限を実機で確かめる。
- API の口(Python/FastAPI)。トークン検証関数(差し替え式、標準は PocketBase)、ファイル・共有 API。
- ファイル共有のフロント(Flet:一覧・アップロード・ダウンロード・共有・招待・共有リンク)。
- ONLYOFFICE Docs 連携。JWT 設定、保存コールバック、document key 管理。
- カレンダー。.ics 保存・Flet の予定 UI・ICS 購読フィード。
- 並行:DocSpace 運営(需要対応・観察)、配り方の決定。
- 次の機能候補:メール(別途設計。本書の決定——独自 ID + メールは属性、連携の口——はメール機能と両立する前提で置いてある)。規模と運用体制で答えが変わるため、選択肢を併記する。前提の事実:〜100 人ではハードは制約にならない(メールは Web より軽く、効く資源はディスクだけ)。制約は送信レピュテーション(一人の事故が全員に波及)と運用の手間。SMTP は送信側が 1〜2 日リトライするので、短時間の停止でメールは消えない——自前ホストに向いた性質。
- 案 1(借りる・最小):メールホスティング(さくらのメールボックス等、年千円台)を借り、蔵側は DNS(MX・SPF・DKIM・DMARC)の設定手順だけ持つ。運用ゼロ。標準 IMAP なので、後で案 3 へ imapsync で移れる——退路つき。
- 案 2(転送のみ・無料):Cloudflare Email Routing で自ドメイン宛を既存のメールボックスへ転送。メールボックスを持たない最薄の形。転送専用で、預け先への依存は残る。
- 案 3(自前・最終形):Stalwart 等を蔵とは別の箱に立て、受信・送信とも自前。「握られない」の到達点。〜100 人なら技術的には余裕だが、送信レート制限・ブロックリスト掲載監視・バックアップ MX・固定 IP の逆引きを仕様に含めること。一人の管理者が片手間で回せる線は経験則で 〜50 人、100 人なら監視の自動化が前提。
- 案 4(折衷):受信は自前(または案 1)、送信だけ外部リレーへ出してレピュテーション管理を外部化。送信事故の影響を台から切り離せる。
- 案 5(借りて取り込む・有力):受信は案 1(さくら等)のまま、蔵が IMAP クライアントとして取り込み、メールを .eml ファイルとしてディレクトリに保存する。メールボックス=ディレクトリ、メール=.eml——カレンダー(.ics)と同型で、権限(xattr)・共有・バックアップ・ファイル API・フロントがそのまま効く。送信はさくらの submission(587)経由。ただし安価なプランの送信は共有 IPで、隣人の巻き添え(遅延・迷惑メール行き)があり得る——受信には共有 IP の影響は一切ない(送り手は MX を引いて配送するだけで、受け側 IP のレピュテーションを見ない)。送信品質が要るなら、受信=借りる / 送信=自前の固定 IP(rDNS + SPF + DKIM 整備)に分担すれば、IP 専有プラン相当を追加費用なしで構成できる(案 4 との組み合わせ)。SPF には両方の送信経路を並べる。危険な部分(25 番の公開・MTA 運用)は借り先に残り、蔵側の攻撃面は増えない。別の箱も不要。Python は標準ライブラリ(imaplib・email)で足り、依存も増えない。一番価値が出るのは共有アドレス(info@ 等)の共有受信トレイ化。個人メールは各自がさくらへ直接 IMAP 接続(取り込みは台が個人のパスワードを預かることになり、グループ単位の権限モデルとも相性が落ちるため、需要が証明されるまで作らない)。退路:.eml は標準形式なので、案 3(自前 Stalwart)へは MX 切替 + インポートでいつでも移れる。
- 迷惑メール判定へのローカル AI 活用(案 5 の発展):判定はローカルの OSS モデルで行う——メールを台の外に出さない(クラウド AI API は全メールを第三者へ送ることになり「握られない」に反する)。安全の設計原則:(1) LLM は判定器に徹する——ツール・権限を与えず、出力はスコア/ラベルに固定(メール本文は攻撃者が書ける入力であり、プロンプトインジェクションは前提)。(2) 削除しない——隔離ディレクトリへの移動のみ、最終判断は人間(業務では偽陽性が最も高くつく)。(3) 段階フィルタ——借り先のフィルタ → SPF/DKIM/DMARC の機械判定で白黒を落とし、グレーゾーンだけ LLM に採点させる(資源浪費攻撃への防御)。(4) LLM に渡すのはサニタイズ済みテキストの先頭のみ——MIME 解析・添付・HTML 処理こそ古典的な攻撃面であり、開かない・実行しない・レンダリングしない。
- 表示の安全既定(案 5 のフロント):既定の操作が安全な操作になるよう設計する。テキスト表示が既定。HTML はレンダリングせずテキスト抽出、明示操作時のみサニタイズ済み HTML(スクリプト除去・リモート画像は読み込まない——画像読み込みは開封確認=追跡)。添付は自動で開かない・プレビューしない——保存されたファイルとして一覧に出るだけで、解釈は利用者の手元で。危険な拡張子は警告表示。リンクは実 URL を隠さず見せる。SPF/DKIM/DMARC の合否を簡単な表示で見せる。「初めての差出人」を表示する(過去の .eml と突き合わせるだけで実装でき、標的型に効く——乗っ取りでない限り攻撃者は初対面になるしかない)。前提:フィルタは雑音除去であって安全保証ではない。優秀なフィルタほど、すり抜けた一通(標的型・乗っ取りアカウント発・新種)への利用者の信頼を不当に高める——大手の「黙って消す」方式と逆に、シグナルを可視化して人間の判断を眠らせない設計を採る。残りは人間の判断(心当たりのない添付・メール内リンクからのログイン・金銭や認証情報の依頼は別経路確認)であり、これは利用者教育として配布物の手引きに含める。
- 書かれた汎用は借り(編集エンジン・認証)、書かれていない固有だけ書く(Workspace 層・権限モデル)。機能は大きく、コードは小さい。
- ファイル管理は、ファイルシステムで。DB という抽象を、核から外す。権限の正は xattr。
- 認証は借り、認可(権限)は自分で持つ。境目の実体はトークン検証関数——だから台ごとに差し替えられる。
- またぐものはつなぐ(Python API が連携の口)、完結するものは現場で(台ごと独立・フェデレーションなし)。
- ファイルベースの正しさは、原子性で守る(flock・atomic rename)。性能の前に、整合性。
- 保存形式はベンダー非依存の primitive に(.ics、JSON、ファイルシステム)。退路は形式が担保する。
- 既知は構造を先に書く。未知(xattr の実用限界、ICS 配信の即時性、台をまたぐ運用、〜100 人規模での Flet 同時セッション資源)は、使いながら育てる。Flet の資源が問題になったら、4.4 の API + Flutter への差し替えが退路。
- AI との接面はファイル。保存形式を素のファイル(markdown・.ics・.eml・JSON・xattr)にしたことで、ローカルの OSS AI が運用の準備(説明の起案・定期予定の生成・台帳の行の起案・現場構成の生成=配り方 C)を読み書きだけで担える。型は迷惑メール判定と同じ:AI は起案、確定は人間(git diff がレビュー画面)、正は常に現実とエンジンの側。ローカルで走らせ、運用情報を台の外に出さない。これは便利の話ではなく前提条件——自前ホストの最大の死因は手作業の累積であり、「走査が宿題を見つけ、AI が下書きし、人は確定するだけ」という循環で人の仕事を「作る」から「確定する」に変えられるかが、一人の管理者が 〜100 人の台を持ち続けられるかを決める。この帯(非定型だが繰り返し現れる、読む・書く・見立てる仕事)は入力が有界・出力の型が固定・誤りは確定段で止まるため、最先端モデルは不要でローカルの OSS モデルで足りる。逆に言えば、運用の循環を外部の AI 購読に依存させない——依存させれば、脱出したはずの握りを運用層で再生産する。フロンティアモデルの出番は設計判断・未知の問題という一度きりの難所に限る。
「配って現場で動かす」を選んだ以上、技術のない現場へどう渡すかまでが設計に入る。重なってよい。
- A. 配布を極小に — 単一に近い構成 + 設定 + USB / スクリプト。バックアップ・移設スクリプト(xattr 対応)を同梱。AGPL の表示要件を確認。
- B. 立てる人を育てる — インストラクター・ネットワークが、現場に立てて渡す。
- C. AI が現場構成を生成 — 要件を書けば、現場用の最小構成を生成。