fixed race condition, added systemctl integration
This commit is contained in:
parent
98e440459b
commit
5e53d0de86
37
README.md
37
README.md
@ -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
24
imap-idle-caldav.service
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user