fixed race condition, added systemctl integration

This commit is contained in:
Thomas Faour 2026-02-12 20:30:10 -05:00
parent 98e440459b
commit 5e53d0de86
3 changed files with 84 additions and 14 deletions

View File

@ -56,6 +56,43 @@ python -m imap_idle_caldav -c config.yaml
The daemon will connect to IMAP, enter IDLE mode, and process incoming calendar emails as they arrive. It re-idles every 5 minutes per RFC 2177 and automatically reconnects on disconnection.
## Systemd Service
Create a service user and install the app:
```bash
sudo useradd -r -s /usr/sbin/nologin imap-idle-caldav
sudo mkdir -p /opt/imap-idle-caldav
sudo python3 -m venv /opt/imap-idle-caldav/.venv
sudo /opt/imap-idle-caldav/.venv/bin/pip install .
```
Put your config in place:
```bash
sudo mkdir -p /etc/imap-idle-caldav
sudo cp config.yaml /etc/imap-idle-caldav/config.yaml
sudo chmod 600 /etc/imap-idle-caldav/config.yaml
sudo chown imap-idle-caldav: /etc/imap-idle-caldav/config.yaml
```
Install and start the service:
```bash
sudo cp imap-idle-caldav.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now imap-idle-caldav
```
Check status and logs:
```bash
sudo systemctl status imap-idle-caldav
sudo journalctl -u imap-idle-caldav -f
```
Edit the `ExecStart` path and `User` in the unit file if your install location or user differs.
## Project Structure
```

24
imap-idle-caldav.service Normal file
View File

@ -0,0 +1,24 @@
[Unit]
Description=IMAP IDLE to CalDAV daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=imap-idle-caldav
ExecStart=/opt/imap-idle-caldav/.venv/bin/imap-idle-caldav -c /etc/imap-idle-caldav/config.yaml
Restart=on-failure
RestartSec=10
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictSUIDSGID=true
[Install]
WantedBy=multi-user.target

View File

@ -10,8 +10,9 @@ from .config import ImapConfig
log = logging.getLogger(__name__)
IDLE_TIMEOUT = 300 # 5 minutes, per RFC 2177
MAX_BACKOFF = 300 # 5 minutes max backoff
IDLE_TIMEOUT = 300 # 5 minutes total IDLE before re-idle (RFC 2177)
IDLE_POLL = 30 # check shutdown every 30 seconds within an IDLE
MAX_BACKOFF = 300 # 5 minutes max backoff
def run_idle_loop(
@ -60,23 +61,31 @@ def _idle_session(
while not (shutdown_event and shutdown_event.is_set()):
client.idle()
try:
responses = client.idle_check(timeout=IDLE_TIMEOUT)
# Poll in short intervals so we can check shutdown_event
# between iterations, rather than blocking for the full
# IDLE_TIMEOUT (which causes a hang on Ctrl-C).
responses = []
deadline = time.monotonic() + IDLE_TIMEOUT
while time.monotonic() < deadline:
if shutdown_event and shutdown_event.is_set():
break
chunk = client.idle_check(timeout=IDLE_POLL)
if chunk:
responses.extend(chunk)
break # got activity, exit IDLE to process
finally:
client.idle_done()
done_result = client.idle_done()
if done_result and done_result[0]:
responses.extend(done_result[0])
if shutdown_event and shutdown_event.is_set():
break
# Check if any EXISTS response indicates new mail
has_new = any(
isinstance(resp, tuple) and len(resp) >= 2 and resp[1] == b"EXISTS"
for resp in responses
)
if not has_new:
continue
# Fetch new messages since last_uid
# Always search for new UIDs, not just when we see EXISTS.
# A message can arrive between idle_done() and the next idle()
# call — the server already sent EXISTS while we weren't
# listening, so we'd never see it. The UID SEARCH is cheap
# and eliminates this race condition entirely.
new_uids = _fetch_new_uids(client, last_uid)
if not new_uids:
continue