This repository has been archived on 2026-09-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nixos-config/hosts/blunix/localServices/orga-agent/client.py
2026-09-03 13:54:21 +02:00

156 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""Unprivileged clients for the local, secret-holding Orga read services."""
import argparse
import json
import socket
import sys
MAX_RESPONSE = 1_000_000
def request(socket_path, payload):
encoded = json.dumps(payload, ensure_ascii=False).encode("utf-8") + b"\n"
if len(encoded) > 16_384:
raise ValueError("request is too large")
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(30)
connection.connect(socket_path)
connection.sendall(encoded)
connection.shutdown(socket.SHUT_WR)
chunks = []
size = 0
while True:
chunk = connection.recv(65_536)
if not chunk:
break
size += len(chunk)
if size > MAX_RESPONSE:
raise ValueError("service response is too large")
chunks.append(chunk)
response = json.loads(b"".join(chunks))
if not response.get("ok"):
raise RuntimeError(response.get("error", "service request failed"))
return response
def calendar_parser():
parser = argparse.ArgumentParser(description="Read the allowlisted Radicale calendar")
parser.add_argument("--socket", default="/run/orga-calendar-read.sock", help=argparse.SUPPRESS)
sub = parser.add_subparsers(dest="operation", required=True)
sub.add_parser("capabilities")
events = sub.add_parser("events")
events.add_argument("--calendar", default="public", choices=["public"])
events.add_argument("--start", required=True, help="ISO-8601 timestamp with timezone")
events.add_argument("--end", required=True, help="ISO-8601 timestamp with timezone")
return parser
def calendar_write_parser():
parser = argparse.ArgumentParser(
description="Create, update or delete Radicale calendar events (full admin, any collection)"
)
parser.add_argument("--socket", default="/run/orga-calendar-write.sock", help=argparse.SUPPRESS)
sub = parser.add_subparsers(dest="operation", required=True)
sub.add_parser("capabilities")
create = sub.add_parser("create")
create.add_argument("--collection", required=True, help="e.g. calendar/public or <user>/<calendar>")
create.add_argument("--title", required=True)
create.add_argument("--start", required=True, help="ISO-8601 timestamp with timezone")
create.add_argument("--end", required=True, help="ISO-8601 timestamp with timezone")
create.add_argument("--description", default="")
create.add_argument("--location", default="")
update = sub.add_parser("update")
update.add_argument("--collection", required=True)
update.add_argument("--uid", required=True, help="concrete event UID -- required, no wildcard updates")
update.add_argument("--title", required=True)
update.add_argument("--start", required=True, help="ISO-8601 timestamp with timezone")
update.add_argument("--end", required=True, help="ISO-8601 timestamp with timezone")
update.add_argument("--description", default="")
update.add_argument("--location", default="")
delete = sub.add_parser("delete")
delete.add_argument("--collection", required=True)
delete.add_argument("--uid", required=True, help="concrete event UID -- required, no wildcard/bulk delete")
return parser
def mail_parser():
parser = argparse.ArgumentParser(description="Read Zoho mail without changing mailbox state")
parser.add_argument("--socket", default="/run/orga-mail-read.sock", help=argparse.SUPPRESS)
sub = parser.add_subparsers(dest="operation", required=True)
sub.add_parser("capabilities")
sub.add_parser("folders")
search = sub.add_parser("search")
search.add_argument("--folder", default="INBOX")
search.add_argument("--since", required=True, help="YYYY-MM-DD")
search.add_argument("--limit", type=int, default=20)
show = sub.add_parser("show")
show.add_argument("--folder", default="INBOX")
show.add_argument("--uid", required=True)
return parser
def mail_draft_parser():
parser = argparse.ArgumentParser(
description="Manage mail drafts in the account's real IMAP Drafts folder "
"(write access limited to that folder; no SMTP, no send)"
)
parser.add_argument("--socket", default="/run/orga-mail-draft.sock", help=argparse.SUPPRESS)
sub = parser.add_subparsers(dest="operation", required=True)
sub.add_parser("capabilities")
sub.add_parser("list")
def add_content_args(target):
target.add_argument("--to", action="append", required=True, help="repeatable, at least one recipient")
target.add_argument("--cc", action="append", default=[])
target.add_argument("--bcc", action="append", default=[])
target.add_argument("--subject", required=True)
target.add_argument("--body", required=True)
target.add_argument("--attachment", action="append", default=[], dest="attachments",
help="repeatable; filename or description, no file content")
create = sub.add_parser("create")
add_content_args(create)
update = sub.add_parser("update")
update.add_argument("--id", required=True)
add_content_args(update)
show = sub.add_parser("show")
show.add_argument("--id", required=True)
delete = sub.add_parser("delete")
delete.add_argument("--id", required=True)
return parser
def main():
if len(sys.argv) < 2 or sys.argv[1] not in {"calendar", "calendar-write", "mail", "mail-draft"}:
print("internal usage: client.py calendar|calendar-write|mail|mail-draft ...", file=sys.stderr)
return 2
capability = sys.argv.pop(1)
if capability == "calendar":
parser = calendar_parser()
elif capability == "calendar-write":
parser = calendar_write_parser()
elif capability == "mail-draft":
parser = mail_draft_parser()
else:
parser = mail_parser()
args = parser.parse_args()
payload = vars(args)
socket_path = payload.pop("socket")
try:
response = request(socket_path, payload)
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as error:
print(f"orga-{capability}: {error}", file=sys.stderr)
return 1
print(json.dumps(response, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
sys.exit(main())