Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,15 @@ gab --db ~/.local/state/github-agent-bridge/bridge.sqlite3 \
run --mode live --workers 2
```

Run the reader separately. Add `--mark-seen` only when the bridge should consume GitHub notifications from the inbox:
Run the reader separately. Add `--mark-seen` only when the bridge should consume GitHub notifications from the configured mailbox:

```bash
gab --db ~/.local/state/github-agent-bridge/bridge.sqlite3 \
--policy ~/.config/github-agent-bridge/policy.json \
read-imap-once \
--email "$GITHUB_AGENT_BRIDGE_EMAIL" \
--password "$GITHUB_AGENT_BRIDGE_PASSWORD" \
--mailbox "$GITHUB_AGENT_BRIDGE_MAILBOX" \
--mark-seen
```

Expand Down
1 change: 1 addition & 0 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ gab --db ~/.local/state/github-agent-bridge/bridge.sqlite3 \
read-imap-once \
--email "$GITHUB_AGENT_BRIDGE_EMAIL" \
--password "$GITHUB_AGENT_BRIDGE_PASSWORD" \
--mailbox "$GITHUB_AGENT_BRIDGE_MAILBOX" \
--mark-seen
```

Expand Down
3 changes: 2 additions & 1 deletion docs/shadow-canary.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Read live IMAP with an independent bridge DB cursor, but do **not** mark message

```bash
gab --db ~/.local/state/github-agent-bridge-shadow/bridge.sqlite3 read-imap-once \
--email "$EMAIL" --password "$APP_PASSWORD"
--email "$EMAIL" --password "$APP_PASSWORD" \
--mailbox "${GITHUB_AGENT_BRIDGE_MAILBOX:-INBOX}"
gab --db ~/.local/state/github-agent-bridge-shadow/bridge.sqlite3 run --mode shadow --once --workers 4
```

Expand Down
6 changes: 3 additions & 3 deletions src/github_agent_bridge/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .parser import decode_header_value, extract_body_text, parse_auth_results
from .policy import Policy
from .queue import JobQueue
from .reader import ImapConfig, ImapReader
from .reader import ImapConfig, ImapReader, imap_mailbox_arg

DEFAULT_DB = os.path.expanduser("~/.local/state/github-agent-bridge/bridge.sqlite3")
DEFAULT_POLICY = os.path.expanduser("~/.config/github-agent-bridge/policy.json")
Expand Down Expand Up @@ -136,7 +136,7 @@ def cmd_replay(args: argparse.Namespace) -> int:

def cmd_read_imap_once(args: argparse.Namespace) -> int:
q = JobQueue(args.db); policy = load_policy(args.policy)
cfg = ImapConfig(args.imap_host, args.imap_port, args.email, args.password, args.mailbox)
cfg = ImapConfig(args.imap_host, args.imap_port, args.email, args.password, imap_mailbox_arg(args.mailbox))
count = ImapReader(cfg, q, policy, mark_seen=args.mark_seen).fetch_once()
print(json.dumps({"enqueued_or_seen": count, "mark_seen": args.mark_seen}, ensure_ascii=False))
return 0
Expand Down Expand Up @@ -297,7 +297,7 @@ def build_parser() -> argparse.ArgumentParser:
s.add_argument("--imap-port", type=int, default=int(os.getenv("GITHUB_AGENT_BRIDGE_IMAP_PORT", "993")))
s.add_argument("--email", default=os.getenv("GITHUB_AGENT_BRIDGE_EMAIL", ""))
s.add_argument("--password", default=os.getenv("GITHUB_AGENT_BRIDGE_PASSWORD", ""))
s.add_argument("--mailbox", default="INBOX"); s.add_argument("--mark-seen", action="store_true", help="mark GitHub notifications as seen; leave off for shadow mode")
s.add_argument("--mailbox", default=os.getenv("GITHUB_AGENT_BRIDGE_MAILBOX", "INBOX")); s.add_argument("--mark-seen", action="store_true", help="mark GitHub notifications as seen; leave off for shadow mode")
s.set_defaults(func=cmd_read_imap_once)
s = sub.add_parser("run")
s.add_argument("--mode", choices=[m.value for m in RunMode], default=RunMode.SHADOW.value)
Expand Down
9 changes: 8 additions & 1 deletion src/github_agent_bridge/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
from .queue import JobQueue


def imap_mailbox_arg(value: str) -> str:
"""Quote mailbox names with spaces for imaplib.select."""
if " " in value and not value.startswith('"'):
return f'"{value}"'
return value


@dataclass(frozen=True)
class ImapConfig:
host: str
Expand Down Expand Up @@ -37,7 +44,7 @@ def fetch_once(self) -> int:
imap = imaplib.IMAP4_SSL(self.config.host, self.config.port)
try:
imap.login(self.config.username, self.config.password)
imap.select(self.config.mailbox)
imap.select(imap_mailbox_arg(self.config.mailbox))
status, data = imap.uid("search", None, f"UID {last_uid + 1}:*")
if status != "OK" or not data or not data[0]:
return 0
Expand Down
8 changes: 1 addition & 7 deletions src/github_agent_bridge/reader_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,13 @@
import sys

from .cli import DEFAULT_DB, DEFAULT_POLICY, main as cli_main
from .reader import imap_mailbox_arg


def env(name: str, default: str = "") -> str:
return os.getenv(name, default)


def imap_mailbox_arg(value: str) -> str:
"""Quote Gmail-style mailbox names with spaces for imaplib.select."""
if " " in value and not value.startswith('"'):
return f'"{value}"'
return value


def main() -> int:
"""Run one IMAP reader pass from GITHUB_AGENT_BRIDGE_* environment.

Expand Down
2 changes: 2 additions & 0 deletions systemd/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ GITHUB_AGENT_BRIDGE_EMAIL=you@example.com
GITHUB_AGENT_BRIDGE_PASSWORD=imap-app-password
GITHUB_AGENT_BRIDGE_IMAP_HOST=imap.gmail.com
GITHUB_AGENT_BRIDGE_IMAP_PORT=993
# IMAP folder/mailbox to scan. For shared accounts this can be a dedicated
# folder such as GitHub or "[Gmail]/All Mail".
GITHUB_AGENT_BRIDGE_MAILBOX=INBOX
# Leave empty for shadow reads; set to --mark-seen only when this bridge owns GitHub notifications.
GITHUB_AGENT_BRIDGE_MARK_SEEN=
Expand Down
40 changes: 39 additions & 1 deletion tests/test_reader_run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from github_agent_bridge import reader_run
from github_agent_bridge import cli, reader_run


def test_reader_run_builds_imap_args_without_mark_seen(monkeypatch):
Expand Down Expand Up @@ -87,6 +87,44 @@ def fake_cli_main(argv):
assert captured["argv"][captured["argv"].index("--mailbox") + 1] == '"[Gmail]/All Mail"'


def test_read_imap_cli_defaults_mailbox_from_env(monkeypatch):
monkeypatch.setenv("GITHUB_AGENT_BRIDGE_MAILBOX", "Shared GitHub")

args = cli.build_parser().parse_args(["read-imap-once"])

assert args.mailbox == "Shared GitHub"


def test_read_imap_cli_quotes_mailbox_with_spaces(monkeypatch, tmp_path, capsys):
captured = {}

class FakeReader:
def __init__(self, cfg, queue, policy, mark_seen=False):
captured["mailbox"] = cfg.mailbox

def fetch_once(self):
return 0

monkeypatch.setattr(cli, "ImapReader", FakeReader)
monkeypatch.setattr(cli, "load_policy", lambda path: object())

rc = cli.main([
"--db",
str(tmp_path / "bridge.sqlite3"),
"read-imap-once",
"--email",
"bot@example.com",
"--password",
"secret",
"--mailbox",
"[Gmail]/All Mail",
])

assert rc == 0
assert captured["mailbox"] == '"[Gmail]/All Mail"'
assert '"enqueued_or_seen": 0' in capsys.readouterr().out


def test_reader_run_requires_email_and_password(monkeypatch, capsys):
monkeypatch.delenv("GITHUB_AGENT_BRIDGE_EMAIL", raising=False)
monkeypatch.delenv("GITHUB_AGENT_BRIDGE_PASSWORD", raising=False)
Expand Down
Loading