Monitor an IMAP mailbox via IDLE for iMIP emails (invitations, RSVPs, cancellations) and ICS attachments, then update a CalDAV calendar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
2.6 KiB
Python
105 lines
2.6 KiB
Python
"""YAML configuration loading and validation."""
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
@dataclass
|
|
class ImapConfig:
|
|
host: str
|
|
port: int
|
|
username: str
|
|
password: str
|
|
folder: str = "INBOX"
|
|
ssl: bool = True
|
|
|
|
|
|
@dataclass
|
|
class CaldavConfig:
|
|
url: str
|
|
username: str
|
|
password: str
|
|
|
|
|
|
@dataclass
|
|
class LoggingConfig:
|
|
level: str = "INFO"
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
imap: ImapConfig
|
|
caldav: CaldavConfig
|
|
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
|
|
|
|
|
def load_config(path: Path) -> Config:
|
|
"""Load and validate configuration from a YAML file."""
|
|
if not path.exists():
|
|
print(f"Error: config file not found: {path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
with open(path) as f:
|
|
raw = yaml.safe_load(f)
|
|
|
|
if not isinstance(raw, dict):
|
|
print("Error: config file must be a YAML mapping", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
for section in ("imap", "caldav"):
|
|
if section not in raw:
|
|
print(f"Error: missing required config section: {section}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
imap_raw = raw["imap"]
|
|
for key in ("host", "username", "password"):
|
|
if key not in imap_raw:
|
|
print(f"Error: missing required imap config key: {key}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
caldav_raw = raw["caldav"]
|
|
for key in ("url", "username", "password"):
|
|
if key not in caldav_raw:
|
|
print(f"Error: missing required caldav config key: {key}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
imap_cfg = ImapConfig(
|
|
host=imap_raw["host"],
|
|
port=imap_raw.get("port", 993),
|
|
username=imap_raw["username"],
|
|
password=imap_raw["password"],
|
|
folder=imap_raw.get("folder", "INBOX"),
|
|
ssl=imap_raw.get("ssl", True),
|
|
)
|
|
|
|
caldav_cfg = CaldavConfig(
|
|
url=caldav_raw["url"],
|
|
username=caldav_raw["username"],
|
|
password=caldav_raw["password"],
|
|
)
|
|
|
|
logging_raw = raw.get("logging", {})
|
|
logging_cfg = LoggingConfig(
|
|
level=logging_raw.get("level", "INFO"),
|
|
)
|
|
|
|
return Config(imap=imap_cfg, caldav=caldav_cfg, logging=logging_cfg)
|
|
|
|
|
|
def parse_args() -> Path:
|
|
"""Parse CLI arguments and return the config file path."""
|
|
parser = argparse.ArgumentParser(
|
|
description="IMAP IDLE to CalDAV daemon",
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--config",
|
|
default="config.yaml",
|
|
help="Path to YAML config file (default: config.yaml)",
|
|
)
|
|
args = parser.parse_args()
|
|
return Path(args.config)
|