contacts: support multiple address book collections

Migadu exposes "family" and "business" address books via CardDAV.
The contacts script now searches all subdirs under contacts/ and
adds new contacts to the "family" collection by default.
This commit is contained in:
Yanxin Lu
2026-03-31 13:33:04 -07:00
parent 3825f3dcdb
commit 0910bd5d5c
4 changed files with 20 additions and 15 deletions

View File

@@ -39,7 +39,7 @@ $CONTACTS_DIR/scripts/contacts.sh list
- [ ] `contact add` prints "Added contact: ..."
- [ ] Second `contact add` prints "Updated contact: ... — added ..."
- [ ] `contact list` shows all contacts with email types
- [ ] `.vcf` files created in `~/.openclaw/workspace/contacts/default/`
- [ ] `.vcf` files created in `~/.openclaw/workspace/contacts/family/`
## 2. Recipient Resolution (Send Validation)

View File

@@ -18,7 +18,7 @@ LLMs can hallucinate email addresses — inventing plausible-looking addresses f
- Python 3 (no external dependencies)
- `vdirsyncer` configured with a CardDAV pair for contacts sync
- Contacts directory: `~/.openclaw/workspace/contacts/default/`
- Contacts directory: `~/.openclaw/workspace/contacts/` (subdirs per address book, e.g. `family/`, `business/`)
## Usage
@@ -64,4 +64,4 @@ Unknown addresses are **rejected** with the available contacts list shown.
## Data
Contacts are stored as vCard 3.0 `.vcf` files in `~/.openclaw/workspace/contacts/default/`, synced to Migadu CardDAV via vdirsyncer. Filenames are `{uid}.vcf`.
Contacts are stored as vCard 3.0 `.vcf` files in `~/.openclaw/workspace/contacts/<collection>/`, synced to Migadu CardDAV via vdirsyncer. Collections match Migadu address books (e.g. `family/`, `business/`). New contacts are added to `family/` by default. The resolve command searches all collections.

View File

@@ -22,7 +22,8 @@ from pathlib import Path
# Config
# ---------------------------------------------------------------------------
CONTACTS_DIR = Path.home() / ".openclaw" / "workspace" / "contacts" / "default"
CONTACTS_ROOT = Path.home() / ".openclaw" / "workspace" / "contacts"
DEFAULT_COLLECTION = "family" # default address book for new contacts
PRODID = "-//OpenClaw//Contacts//EN"
# ---------------------------------------------------------------------------
@@ -57,17 +58,20 @@ def _parse_vcf(path):
def _load_contacts():
"""Load all contacts from CONTACTS_DIR. Returns list of parsed contact dicts."""
if not CONTACTS_DIR.is_dir():
"""Load all contacts from all collections under CONTACTS_ROOT."""
if not CONTACTS_ROOT.is_dir():
return []
contacts = []
for vcf_path in sorted(CONTACTS_DIR.glob("*.vcf")):
try:
contact = _parse_vcf(vcf_path)
if contact["fn"] or contact["emails"]:
contacts.append(contact)
except Exception:
for subdir in sorted(CONTACTS_ROOT.iterdir()):
if not subdir.is_dir():
continue
for vcf_path in sorted(subdir.glob("*.vcf")):
try:
contact = _parse_vcf(vcf_path)
if contact["fn"] or contact["emails"]:
contacts.append(contact)
except Exception:
continue
return contacts
@@ -225,7 +229,8 @@ def cmd_list(args):
def cmd_add(args):
"""Add a new contact (creates or updates a .vcf file)."""
CONTACTS_DIR.mkdir(parents=True, exist_ok=True)
target_dir = CONTACTS_ROOT / DEFAULT_COLLECTION
target_dir.mkdir(parents=True, exist_ok=True)
emails = [{"address": args.email, "type": args.type or ""}]
nickname = args.nickname or ""
@@ -251,7 +256,7 @@ def cmd_add(args):
# New contact
uid = f"{uuid.uuid4()}@openclaw"
vcf_str = _build_vcf(args.name, emails, nickname, uid)
dest = CONTACTS_DIR / f"{uid}.vcf"
dest = target_dir / f"{uid}.vcf"
dest.write_text(vcf_str, encoding="utf-8")
print(f"Added contact: {args.name} <{args.email}>")
_sync_contacts()