Add message retention and hide emails by default

- db: cleanup_old_messages(days) purges messages older than N days in
  batches; recipients/reads/deliveries/reactions follow via ON DELETE
  CASCADE. Returns attachment file_ids no longer referenced by any
  surviving message (forwarded copies keep their files) and removes
  their image_uploads rows
- server: MESSAGE_RETENTION_DAYS env var (default 0 = keep forever);
  hourly cleanup deletes expired messages and securely removes orphaned
  attachment blobs from the upload dir
- schema: email_visible now defaults to 0 — previously any logged-in
  user who knew a UUID could read another user's email via get_profile
- migrations: SQL script to apply the new default and reset the flag on
  existing databases (run manually, see file header)
- docker-compose: document MESSAGE_RETENTION_DAYS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Filip
2026-06-12 10:30:42 +02:00
parent 750290ddc1
commit f42ecf5c5b
5 changed files with 118 additions and 1 deletions

View File

@@ -203,6 +203,10 @@ CONNECTION_RL_MAX = 20 # max requests per window per connection
MAX_CONNECTIONS_PER_IP = 10
MAX_CONNECTIONS_GLOBAL = 200
METADATA_RETENTION_DAYS = int(os.getenv("METADATA_RETENTION_DAYS", "90"))
# Message retention: 0 (default) keeps messages forever; N > 0 purges
# messages (and their per-recipient ciphertexts, deliveries, reads,
# reactions and orphaned attachment blobs) older than N days.
MESSAGE_RETENTION_DAYS = int(os.getenv("MESSAGE_RETENTION_DAYS", "0"))
# TCP keepalive settings (seconds)
TCP_KEEPALIVE_IDLE = 25 # Start keepalive probes after 25s of idle
TCP_KEEPALIVE_INTERVAL = 10 # Send probes every 10s
@@ -3177,6 +3181,18 @@ async def main():
reads_del, reactions_del)
except Exception as e:
logger.warning("Metadata cleanup error: %s", e)
if MESSAGE_RETENTION_DAYS > 0:
try:
msgs_del, orphan_files = await adb.cleanup_old_messages(MESSAGE_RETENTION_DAYS)
for fid in orphan_files:
p = _safe_upload_path(fid, ".enc")
if p:
await asyncio.to_thread(_secure_delete, p)
if msgs_del or orphan_files:
logger.info("Message retention: %d messages, %d attachment files purged",
msgs_del, len(orphan_files))
except Exception as e:
logger.warning("Message retention cleanup error: %s", e)
asyncio.create_task(_periodic_cleanup())