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
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,20 @@ sudo mkdir -p /usr/local/inspamity
# Clone the repository (or copy your files)
sudo git clone https://github.com/dtump/inspamity.git /usr/local/inspamity

# Set proper permissions
sudo chmod +x /usr/local/inspamity/email_ai_interface.py
sudo chmod +x /usr/local/inspamity/cli_toolbox.py
# Set executable permissions
sudo chmod 0755 /usr/local/inspamity/email_ai_interface.py
sudo chmod 0755 /usr/local/inspamity/cli_toolbox.py

# Create a virtual environment and install dependencies
sudo python3 -m venv /usr/local/inspamity/.venv
sudo /usr/local/inspamity/.venv/bin/pip install /usr/local/inspamity

# Create system config and private debug locations
sudo install -d -o root -g _rspamd -m 0750 /etc/inspamity
sudo install -d -o _rspamd -g _rspamd -m 0700 /var/local/inspamity
sudo cp /usr/local/inspamity/config.ini.default /etc/inspamity/config.ini
sudo chown root:_rspamd /etc/inspamity/config.ini
sudo chmod 0640 /etc/inspamity/config.ini
```

### Step 2: Install the rspamd integration
Expand All @@ -65,7 +72,8 @@ sudo cp /usr/local/inspamity/rspamd/external_ai_test.lua /etc/rspamd/plugins.d/
# Create /etc/rspamd/modules.d/external_ai_test.conf
echo -e 'external_ai_test {\n enabled = true;\n}' | sudo tee /etc/rspamd/modules.d/external_ai_test.conf

# Restart rspamd to apply changes
# Verify rspamd config, then restart rspamd to apply changes
sudo rspamadm configtest
sudo systemctl restart rspamd
```

Expand All @@ -81,24 +89,32 @@ sudo tail -f /var/log/rspamd/rspamd.log

### Step 4: Enable debugging

```bash
# Make directory for debugging data
mkdir /var/local/inspamity
Debug mode can save raw email, processed email, AI output, and error logs. Treat these files as private mail data.

# Make _rspamd owner of this directory
chown _rspamd: /var/local/inspamity
```bash
# The installation step above creates this directory; these commands are safe to re-run.
sudo install -d -o _rspamd -g _rspamd -m 0700 /var/local/inspamity
```

After this, enable debugging in the config.ini
After this, set `debug_mode = true` in `/etc/inspamity/config.ini` if you really need debug artifacts. Inspamity creates new debug files with mode `0600`.

## ⚙️ Configuration

Copy `config.ini.default` to `config.ini` (or `/etc/inspamity/config.ini` for system-wide) and edit it:
Copy `config.ini.default` to `/etc/inspamity/config.ini` for system-wide production use and edit it. Keep it readable only by root and the rspamd runtime user because it contains provider API keys:

```bash
sudo chown root:_rspamd /etc/inspamity/config.ini
sudo chmod 0640 /etc/inspamity/config.ini
sudo -u _rspamd test -r /etc/inspamity/config.ini
```

```ini
[settings]
# AI provider: anthropic or openai
provider = anthropic
# Save private debug artifacts under debug_directory when true
debug_mode = false
debug_directory = /var/local/inspamity

[anthropic]
api_key = your_api_key_here
Expand Down Expand Up @@ -130,7 +146,7 @@ By default, the rspamd Lua script is configured to:
- Apply a score based on the AI's confidence level
- Log detailed information for debugging

You can adjust these settings by editing `/etc/rspamd/local.d/external_ai_test.lua` to fit your needs.
You can adjust these settings by editing the installed plugin at `/etc/rspamd/plugins.d/external_ai_test.lua` to fit your needs.

## 📊 How It Works

Expand All @@ -144,7 +160,10 @@ You can adjust these settings by editing `/etc/rspamd/local.d/external_ai_test.l

- Check rspamd logs for errors: `sudo tail -f /var/log/rspamd/rspamd.log`
- Test email processing directly: `/usr/local/inspamity/.venv/bin/python3 /usr/local/inspamity/email_ai_interface.py email.eml`
- Verify permissions on all scripts and directories
- Verify permissions on all scripts and directories:
- `/etc/inspamity` should be `0750 root:_rspamd`
- `/etc/inspamity/config.ini` should be `0640 root:_rspamd`
- `/var/local/inspamity` should be `0700 _rspamd:_rspamd`
- Ensure all dependencies are properly installed

## 📜 License
Expand Down
7 changes: 6 additions & 1 deletion config.ini.default
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Production config is normally /etc/inspamity/config.ini.
# Recommended permissions: /etc/inspamity 0750 root:_rspamd, config.ini 0640 root:_rspamd.

[settings]
# AI provider to use: anthropic or openai
provider = anthropic
# Enable debug mode to save copies of processed emails and AI output
debug_mode = false
# Directory where debug files will be stored
# Directory where debug files will be stored.
# Treat this as private mail data; recommended permissions: 0700 _rspamd:_rspamd.
# New debug files are created with mode 0600.
debug_directory = /var/local/inspamity

[anthropic]
Expand Down
61 changes: 49 additions & 12 deletions email_ai_interface.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
#!/usr/bin/env python3
import json
import os
import sys
import traceback
from datetime import datetime
from pathlib import Path
from typing import Any

from email_utils.ai_spam_check import check_spam_with_ai
from email_utils.config import load_config
from email_utils.process_email import format_email_content, get_email_content

DEBUG_DIR_MODE = 0o700
DEBUG_FILE_MODE = 0o600


def prepare_debug_directory(debug_dir: str) -> Path:
"""Create and lock down the debug directory for private mail artifacts."""
debug_path = Path(debug_dir)
debug_path.mkdir(parents=True, mode=DEBUG_DIR_MODE, exist_ok=True)
debug_path.chmod(DEBUG_DIR_MODE)
return debug_path


def write_private_text(path: Path, content: str) -> None:
"""Write a debug text file without allowing group/other access."""
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
fd = os.open(path, flags, DEBUG_FILE_MODE)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
fd = -1
f.write(content)
finally:
if fd != -1:
os.close(fd)
path.chmod(DEBUG_FILE_MODE)


def write_private_json(path: Path, payload: dict[str, Any]) -> None:
"""Write a debug JSON file without allowing group/other access."""
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
fd = os.open(path, flags, DEBUG_FILE_MODE)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
fd = -1
json.dump(payload, f, indent=2)
finally:
if fd != -1:
os.close(fd)
path.chmod(DEBUG_FILE_MODE)


def main() -> None:
try:
Expand All @@ -32,12 +73,10 @@ def main() -> None:
raise ValueError("Empty email input received")

if debug_mode:
debug_path = Path(debug_dir)
debug_path.mkdir(parents=True, exist_ok=True)
debug_path = prepare_debug_directory(debug_dir)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")

with open(debug_path / f"raw_email_{timestamp}.eml", "w", encoding="utf-8") as f:
f.write(email_content)
write_private_text(debug_path / f"raw_email_{timestamp}.eml", email_content)

# Process the email
processed_email = get_email_content(email_content, is_string=True)
Expand All @@ -47,21 +86,19 @@ def main() -> None:
ai_result = check_spam_with_ai(formatted_output)

if debug_mode:
with open(debug_path / f"processed_email_{timestamp}.txt", "w", encoding="utf-8") as f:
f.write(formatted_output)

with open(debug_path / f"ai_output_{timestamp}.json", "w", encoding="utf-8") as f:
json.dump(ai_result, f, indent=2)
write_private_text(debug_path / f"processed_email_{timestamp}.txt", formatted_output)
write_private_json(debug_path / f"ai_output_{timestamp}.json", ai_result)

print(json.dumps(ai_result))
sys.exit(0)

except Exception as e:
if "debug_mode" in locals() and debug_mode and "debug_path" in locals():
error_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
with open(debug_path / f"error_{error_timestamp}.log", "w", encoding="utf-8") as f:
f.write(f"Error: {e}\n\n")
f.write(traceback.format_exc())
write_private_text(
debug_path / f"error_{error_timestamp}.log",
f"Error: {e}\n\n{traceback.format_exc()}",
)

error_json = {"error": True, "message": str(e), "timestamp": datetime.now().isoformat()}
print(json.dumps(error_json), file=sys.stderr)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_email_ai_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import json
import stat

from email_ai_interface import prepare_debug_directory, write_private_json, write_private_text


def mode(path):
return stat.S_IMODE(path.stat().st_mode)


def test_prepare_debug_directory_uses_private_mode(tmp_path):
debug_dir = tmp_path / "debug"

prepare_debug_directory(str(debug_dir))

assert mode(debug_dir) == 0o700


def test_prepare_debug_directory_tightens_existing_mode(tmp_path):
debug_dir = tmp_path / "debug"
debug_dir.mkdir(mode=0o755)

prepare_debug_directory(str(debug_dir))

assert mode(debug_dir) == 0o700


def test_write_private_text_uses_private_mode(tmp_path):
debug_file = tmp_path / "raw_email_test.eml"

write_private_text(debug_file, "private mail")

assert debug_file.read_text() == "private mail"
assert mode(debug_file) == 0o600


def test_write_private_json_uses_private_mode(tmp_path):
debug_file = tmp_path / "ai_output_test.json"
payload = {"is_spam": "no", "confidence": 99, "reason": "test"}

write_private_json(debug_file, payload)

assert json.loads(debug_file.read_text()) == payload
assert mode(debug_file) == 0o600
Loading