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.
|
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
|
## 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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
IDLE_TIMEOUT = 300 # 5 minutes, per RFC 2177
|
IDLE_TIMEOUT = 300 # 5 minutes total IDLE before re-idle (RFC 2177)
|
||||||
MAX_BACKOFF = 300 # 5 minutes max backoff
|
IDLE_POLL = 30 # check shutdown every 30 seconds within an IDLE
|
||||||
|
MAX_BACKOFF = 300 # 5 minutes max backoff
|
||||||
|
|
||||||
|
|
||||||
def run_idle_loop(
|
def run_idle_loop(
|
||||||
@ -60,23 +61,31 @@ def _idle_session(
|
|||||||
while not (shutdown_event and shutdown_event.is_set()):
|
while not (shutdown_event and shutdown_event.is_set()):
|
||||||
client.idle()
|
client.idle()
|
||||||
try:
|
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:
|
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():
|
if shutdown_event and shutdown_event.is_set():
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if any EXISTS response indicates new mail
|
# Always search for new UIDs, not just when we see EXISTS.
|
||||||
has_new = any(
|
# A message can arrive between idle_done() and the next idle()
|
||||||
isinstance(resp, tuple) and len(resp) >= 2 and resp[1] == b"EXISTS"
|
# call — the server already sent EXISTS while we weren't
|
||||||
for resp in responses
|
# listening, so we'd never see it. The UID SEARCH is cheap
|
||||||
)
|
# and eliminates this race condition entirely.
|
||||||
|
|
||||||
if not has_new:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Fetch new messages since last_uid
|
|
||||||
new_uids = _fetch_new_uids(client, last_uid)
|
new_uids = _fetch_new_uids(client, last_uid)
|
||||||
if not new_uids:
|
if not new_uids:
|
||||||
continue
|
continue
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user