feat: add apprise notification delivery
This commit is contained in:
@@ -21,6 +21,7 @@ from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.core.security import require_admin_auth
|
||||
from app.models.setting import Setting
|
||||
from app.services.notifications import send_notification
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -113,66 +114,25 @@ SETTING_DEFAULTS: List[Dict[str, Any]] = [
|
||||
},
|
||||
# ── Notifications ─────────────────────────────────────────────────────────
|
||||
{
|
||||
"key": "notifications.email_enabled",
|
||||
"key": "notifications.apprise_enabled",
|
||||
"value": "false",
|
||||
"description": "Send email notifications when new DMARC failures are detected",
|
||||
"description": "Send notifications through configured Apprise target URLs",
|
||||
"value_type": "boolean",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.email_from",
|
||||
"key": "notifications.apprise_urls",
|
||||
"value": "",
|
||||
"description": "From address used for notification emails",
|
||||
"description": "Newline-separated Apprise notification target URLs",
|
||||
"value_type": "string",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.email_to",
|
||||
"value": "",
|
||||
"description": "Comma-separated list of recipient addresses for notifications",
|
||||
"value_type": "string",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.smtp_host",
|
||||
"value": "",
|
||||
"description": "SMTP server hostname for sending notification emails",
|
||||
"value_type": "string",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.smtp_port",
|
||||
"value": "587",
|
||||
"description": "SMTP server port",
|
||||
"value_type": "integer",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.smtp_username",
|
||||
"value": "",
|
||||
"description": "SMTP authentication username",
|
||||
"value_type": "string",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.smtp_password",
|
||||
"value": "",
|
||||
"description": "SMTP authentication password",
|
||||
"value_type": "string",
|
||||
"category": "notifications",
|
||||
},
|
||||
{
|
||||
"key": "notifications.smtp_use_tls",
|
||||
"value": "true",
|
||||
"description": "Use TLS when connecting to the SMTP server",
|
||||
"value_type": "boolean",
|
||||
"category": "notifications",
|
||||
},
|
||||
]
|
||||
|
||||
# Keys whose values should be redacted in GET responses (treated as secrets)
|
||||
_SECRET_KEYS = {
|
||||
"cloudflare.api_token",
|
||||
"notifications.apprise_urls",
|
||||
"notifications.smtp_password",
|
||||
}
|
||||
|
||||
@@ -245,6 +205,17 @@ class SettingResponse(BaseModel):
|
||||
updated_at: Optional[str]
|
||||
|
||||
|
||||
class NotificationTestResponse(BaseModel):
|
||||
"""Sanitized response from a test notification send."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
configured_targets: int = 0
|
||||
invalid_targets: int = 0
|
||||
skipped: bool = False
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -269,6 +240,27 @@ async def list_settings(
|
||||
return [_row_to_dict(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/notifications/test", response_model=NotificationTestResponse)
|
||||
async def test_notification_settings(
|
||||
db: Session = Depends(get_db),
|
||||
_auth: dict = Depends(require_admin_auth),
|
||||
) -> NotificationTestResponse:
|
||||
"""Send a test notification using the configured Apprise targets."""
|
||||
_seed_defaults(db)
|
||||
result = send_notification(
|
||||
db,
|
||||
title="DMARQ test notification",
|
||||
body="This confirms that DMARQ can reach the configured notification target.",
|
||||
force=True,
|
||||
)
|
||||
if not result.success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=result.to_dict(),
|
||||
)
|
||||
return result.to_dict()
|
||||
|
||||
|
||||
@router.get("/{key:path}", response_model=SettingResponse)
|
||||
async def get_setting(
|
||||
key: str,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Notification delivery helpers backed by Apprise."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import apprise
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.setting import Setting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationResult:
|
||||
"""Sanitized result for a notification send attempt."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
configured_targets: int = 0
|
||||
invalid_targets: int = 0
|
||||
skipped: bool = False
|
||||
error: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, object]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def _truthy(value: Optional[str]) -> bool:
|
||||
return str(value or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _split_apprise_urls(value: Optional[str]) -> List[str]:
|
||||
if not value:
|
||||
return []
|
||||
return [
|
||||
line.strip()
|
||||
for line in value.splitlines()
|
||||
if line.strip() and not line.strip().startswith("#")
|
||||
]
|
||||
|
||||
|
||||
def _notification_settings(db: Session) -> Dict[str, Optional[str]]:
|
||||
rows = db.query(Setting).filter(Setting.category == "notifications").all()
|
||||
return {row.key: row.value for row in rows}
|
||||
|
||||
|
||||
def _add_apprise_targets(notifier: apprise.Apprise, urls: List[str]) -> Tuple[int, int]:
|
||||
configured_targets = 0
|
||||
invalid_targets = 0
|
||||
for url in urls:
|
||||
try:
|
||||
if notifier.add(url):
|
||||
configured_targets += 1
|
||||
else:
|
||||
invalid_targets += 1
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
invalid_targets += 1
|
||||
logger.warning("Invalid Apprise notification target was ignored.")
|
||||
return configured_targets, invalid_targets
|
||||
|
||||
|
||||
def send_notification(
|
||||
db: Session,
|
||||
*,
|
||||
title: str,
|
||||
body: str,
|
||||
force: bool = False,
|
||||
) -> NotificationResult:
|
||||
"""Send a notification through configured Apprise target URLs."""
|
||||
settings = _notification_settings(db)
|
||||
enabled = _truthy(settings.get("notifications.apprise_enabled"))
|
||||
|
||||
if not enabled and not force:
|
||||
return NotificationResult(
|
||||
success=False,
|
||||
skipped=True,
|
||||
message="Notifications are disabled.",
|
||||
)
|
||||
|
||||
urls = _split_apprise_urls(settings.get("notifications.apprise_urls"))
|
||||
if not urls:
|
||||
return NotificationResult(
|
||||
success=False,
|
||||
message="No notification targets are configured.",
|
||||
)
|
||||
|
||||
notifier = apprise.Apprise()
|
||||
configured_targets, invalid_targets = _add_apprise_targets(notifier, urls)
|
||||
|
||||
if configured_targets == 0:
|
||||
return NotificationResult(
|
||||
success=False,
|
||||
message="No valid notification targets are configured.",
|
||||
invalid_targets=invalid_targets,
|
||||
)
|
||||
|
||||
try:
|
||||
success = bool(notifier.notify(title=title, body=body))
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logger.exception("Apprise notification delivery failed.")
|
||||
return NotificationResult(
|
||||
success=False,
|
||||
message="Notification delivery failed.",
|
||||
configured_targets=configured_targets,
|
||||
invalid_targets=invalid_targets,
|
||||
error="delivery_failed",
|
||||
)
|
||||
|
||||
if not success:
|
||||
return NotificationResult(
|
||||
success=False,
|
||||
message="Notification delivery was not accepted by any configured target.",
|
||||
configured_targets=configured_targets,
|
||||
invalid_targets=invalid_targets,
|
||||
error="not_delivered",
|
||||
)
|
||||
|
||||
return NotificationResult(
|
||||
success=True,
|
||||
message="Notification sent.",
|
||||
configured_targets=configured_targets,
|
||||
invalid_targets=invalid_targets,
|
||||
)
|
||||
@@ -204,97 +204,49 @@
|
||||
{% endcall %}
|
||||
{% endcall %}
|
||||
|
||||
<!-- ── Email Notifications ─────────────────────────────────────────────── -->
|
||||
<!-- ── Notifications ──────────────────────────────────────────────────── -->
|
||||
{% call card() %}
|
||||
{% call card_header() %}
|
||||
{% call card_title() %}Email Notifications{% endcall %}
|
||||
{% call card_description() %}Send alerts when DMARC failures are detected{% endcall %}
|
||||
{% call card_title() %}Notifications{% endcall %}
|
||||
{% call card_description() %}Send alerts through Apprise-compatible targets{% endcall %}
|
||||
{% endcall %}
|
||||
{% call card_content() %}
|
||||
<form @submit.prevent="saveCategory('notifications')" class="space-y-4">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox"
|
||||
:checked="s['notifications.email_enabled'] === 'true'"
|
||||
@change="s['notifications.email_enabled'] = $event.target.checked ? 'true' : 'false'"
|
||||
:checked="s['notifications.apprise_enabled'] === 'true'"
|
||||
@change="s['notifications.apprise_enabled'] = $event.target.checked ? 'true' : 'false'"
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="label-text font-medium">Enable email notifications</span>
|
||||
<span class="label-text font-medium">Enable notifications</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template x-if="s['notifications.email_enabled'] === 'true'">
|
||||
<template x-if="s['notifications.apprise_enabled'] === 'true'">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">From Address</span></label>
|
||||
<input type="email" x-model="s['notifications.email_from']"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="noreply@example.com" />
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">Recipient(s)</span></label>
|
||||
<input type="text" x-model="s['notifications.email_to']"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="admin@example.com, security@example.com" />
|
||||
<label class="label"><span class="label-text-alt text-muted-foreground">Comma-separated email addresses</span></label>
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">Apprise Target URLs</span></label>
|
||||
<textarea x-model="s['notifications.apprise_urls']"
|
||||
class="textarea textarea-bordered w-full min-h-32 font-mono text-sm"
|
||||
placeholder="mailto://user:password@example.com slack://token-a/token-b/token-c/channel"></textarea>
|
||||
<label class="label"><span class="label-text-alt text-muted-foreground">One target per line. Values are redacted after saving.</span></label>
|
||||
</div>
|
||||
|
||||
<div class="divider text-sm">SMTP Configuration</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">SMTP Host</span></label>
|
||||
<input type="text" x-model="s['notifications.smtp_host']"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="smtp.example.com" />
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">SMTP Port</span></label>
|
||||
<input type="number" x-model.number="s['notifications.smtp_port']"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="587" min="1" max="65535" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">SMTP Username</span></label>
|
||||
<input type="text" x-model="s['notifications.smtp_username']"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="smtpuser@example.com" />
|
||||
</div>
|
||||
<div class="form-control w-full">
|
||||
<label class="label"><span class="label-text font-medium">SMTP Password</span></label>
|
||||
<div class="relative">
|
||||
<input :type="showSmtpPw ? 'text' : 'password'"
|
||||
x-model="s['notifications.smtp_password']"
|
||||
class="input input-bordered w-full pr-10"
|
||||
placeholder="••••••••" />
|
||||
<button type="button"
|
||||
class="absolute right-2 top-3 text-muted-foreground hover:text-foreground"
|
||||
@click="showSmtpPw = !showSmtpPw">
|
||||
<svg x-show="!showSmtpPw" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
<svg x-show="showSmtpPw" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="label"><span class="label-text-alt text-muted-foreground">Leave as-is to keep existing password</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox"
|
||||
:checked="s['notifications.smtp_use_tls'] === 'true'"
|
||||
@change="s['notifications.smtp_use_tls'] = $event.target.checked ? 'true' : 'false'"
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="label-text font-medium">Use TLS (STARTTLS)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-col sm:flex-row justify-end gap-2">
|
||||
<button type="button" class="btn btn-outline btn-md" :disabled="saving || testingNotification" @click="sendTestNotification()">
|
||||
<template x-if="!testingNotification">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2">
|
||||
<path d="m22 2-7 20-4-9-9-4Z"></path>
|
||||
<path d="M22 2 11 13"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<template x-if="testingNotification"><span class="loading loading-spinner loading-xs mr-2"></span></template>
|
||||
Send Test
|
||||
</button>
|
||||
<button type="submit" class="btn btn-default btn-md" :disabled="saving">
|
||||
<template x-if="!saving">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path><polyline points="17 21 17 13 7 13 7 21"></polyline><polyline points="7 3 7 8 15 8"></polyline></svg>
|
||||
@@ -339,8 +291,8 @@ function settingsApp() {
|
||||
saving: false,
|
||||
flashMsg: '',
|
||||
flashOk: true,
|
||||
testingNotification: false,
|
||||
showCfToken: false,
|
||||
showSmtpPw: false,
|
||||
|
||||
// Session cookie is sent automatically by the browser (httpOnly, same-origin).
|
||||
// No manual auth header needed for API calls from the UI.
|
||||
@@ -395,6 +347,28 @@ function settingsApp() {
|
||||
}
|
||||
},
|
||||
|
||||
async sendTestNotification() {
|
||||
this.testingNotification = true;
|
||||
try {
|
||||
await this.saveCategory('notifications');
|
||||
const res = await fetch('/api/v1/settings/notifications/test', {
|
||||
method: 'POST',
|
||||
headers: this.apiHeaders(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const detail = data.detail || {};
|
||||
this.showFlash('Test failed: ' + (detail.message || data.detail || res.statusText), false);
|
||||
} else {
|
||||
this.showFlash('Test notification sent.', true);
|
||||
}
|
||||
} catch (err) {
|
||||
this.showFlash('Error sending test notification: ' + err.message, false);
|
||||
} finally {
|
||||
this.testingNotification = false;
|
||||
}
|
||||
},
|
||||
|
||||
showFlash(msg, ok) {
|
||||
this.flashMsg = msg;
|
||||
this.flashOk = ok;
|
||||
@@ -403,4 +377,4 @@ function settingsApp() {
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,6 +49,8 @@ class TestSettingsAPI:
|
||||
assert "general.app_name" in keys
|
||||
assert "dmarc.default_policy" in keys
|
||||
assert "cloudflare.api_token" in keys
|
||||
assert "notifications.apprise_enabled" in keys
|
||||
assert "notifications.apprise_urls" in keys
|
||||
|
||||
def test_list_settings_filter_by_category(self, authed_client: TestClient):
|
||||
"""GET /api/v1/settings?category=dmarc returns only dmarc settings."""
|
||||
@@ -125,6 +127,17 @@ class TestSettingsAPI:
|
||||
assert res.status_code == 200
|
||||
assert res.json()["value"] == "**redacted**"
|
||||
|
||||
def test_apprise_urls_are_redacted_in_response(self, authed_client: TestClient):
|
||||
"""Apprise target URLs are treated as notification secrets."""
|
||||
authed_client.get("/api/v1/settings")
|
||||
authed_client.put(
|
||||
"/api/v1/settings/notifications.apprise_urls",
|
||||
json={"value": "mailto://user:password@example.com"},
|
||||
)
|
||||
res = authed_client.get("/api/v1/settings/notifications.apprise_urls")
|
||||
assert res.status_code == 200
|
||||
assert res.json()["value"] == "**redacted**"
|
||||
|
||||
def test_redacted_placeholder_does_not_overwrite(self, authed_client: TestClient):
|
||||
"""Sending **redacted** back to PUT should not overwrite the stored value."""
|
||||
authed_client.get("/api/v1/settings")
|
||||
@@ -148,3 +161,55 @@ class TestSettingsAPI:
|
||||
"""Unauthenticated requests to settings endpoints return 403."""
|
||||
res = client.get("/api/v1/settings")
|
||||
assert res.status_code in (401, 403)
|
||||
|
||||
def test_test_notification_sends_via_apprise(self, authed_client: TestClient, monkeypatch):
|
||||
"""POST /settings/notifications/test sends a sanitized Apprise test notification."""
|
||||
|
||||
class FakeApprise:
|
||||
instances = []
|
||||
|
||||
def __init__(self):
|
||||
self.urls = []
|
||||
self.messages = []
|
||||
FakeApprise.instances.append(self)
|
||||
|
||||
def add(self, url):
|
||||
self.urls.append(url)
|
||||
return True
|
||||
|
||||
def notify(self, *, title, body):
|
||||
self.messages.append({"title": title, "body": body})
|
||||
return True
|
||||
|
||||
monkeypatch.setattr("app.services.notifications.apprise.Apprise", FakeApprise)
|
||||
|
||||
authed_client.get("/api/v1/settings")
|
||||
authed_client.post(
|
||||
"/api/v1/settings/bulk",
|
||||
json={
|
||||
"settings": {
|
||||
"notifications.apprise_enabled": "true",
|
||||
"notifications.apprise_urls": "mailto://user:password@example.com",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
res = authed_client.post("/api/v1/settings/notifications/test")
|
||||
|
||||
assert res.status_code == 200
|
||||
data = res.json()
|
||||
assert data["success"] is True
|
||||
assert data["configured_targets"] == 1
|
||||
assert "password" not in str(data)
|
||||
assert FakeApprise.instances[0].messages[0]["title"] == "DMARQ test notification"
|
||||
|
||||
def test_test_notification_without_targets_returns_400(self, authed_client: TestClient):
|
||||
"""Test notification returns a useful error when no target is configured."""
|
||||
authed_client.get("/api/v1/settings")
|
||||
|
||||
res = authed_client.post("/api/v1/settings/notifications/test")
|
||||
|
||||
assert res.status_code == 400
|
||||
detail = res.json()["detail"]
|
||||
assert detail["success"] is False
|
||||
assert detail["message"] == "No notification targets are configured."
|
||||
|
||||
@@ -74,18 +74,17 @@ For database operations, use the [Database Backup and Restore](backups.md) guide
|
||||
| `MAX_UPLOAD_SIZE` | Maximum file upload size (MB) | `10` | `20`, `50` |
|
||||
| `SESSION_LIFETIME` | Session lifetime in minutes | `1440` (24h) | `60`, `720` |
|
||||
|
||||
### Alerting Configuration
|
||||
### Notification Configuration
|
||||
|
||||
| Variable | Description | Default | Example |
|
||||
|----------|-------------|---------|---------|
|
||||
| `ALERTS_ENABLED` | Enable alerts | `false` | `true`, `false` |
|
||||
| `ALERT_EMAIL` | Email to send alerts to | - | `admin@example.com` |
|
||||
| `SMTP_SERVER` | SMTP server for sending alerts | - | `smtp.gmail.com` |
|
||||
| `SMTP_PORT` | SMTP port | `587` | `587`, `465` |
|
||||
| `SMTP_USERNAME` | SMTP username | - | `alerts@example.com` |
|
||||
| `SMTP_PASSWORD` | SMTP password | - | `smtp_password` |
|
||||
| `SMTP_USE_TLS` | Use TLS for SMTP | `true` | `true`, `false` |
|
||||
| `ALERT_THRESHOLD` | Compliance threshold for alerts | `90` | `80`, `95` |
|
||||
DMARQ stores notification targets in the web settings table. Configure them under
|
||||
**Settings** > **Notifications** and use newline-separated Apprise URLs, such as
|
||||
email, Slack, Teams, Discord, or webhook targets. Saved target URLs are redacted
|
||||
from API responses.
|
||||
|
||||
| Setting | Description | Default | Example |
|
||||
|---------|-------------|---------|---------|
|
||||
| `notifications.apprise_enabled` | Enable Apprise notification delivery | `false` | `true` |
|
||||
| `notifications.apprise_urls` | Newline-separated Apprise target URLs | - | `mailto://user:pass@example.com` |
|
||||
|
||||
### Cloudflare Integration
|
||||
|
||||
@@ -143,18 +142,12 @@ IMAP_POLLING_INTERVAL=30
|
||||
IMAP_MARK_AS_READ=true
|
||||
```
|
||||
|
||||
### With Email Alerting
|
||||
### With Apprise Notifications
|
||||
|
||||
```
|
||||
ALERTS_ENABLED=true
|
||||
ALERT_EMAIL=admin@example.com
|
||||
SMTP_SERVER=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=alerts@example.com
|
||||
SMTP_PASSWORD=smtp_password
|
||||
SMTP_USE_TLS=true
|
||||
ALERT_THRESHOLD=95
|
||||
```
|
||||
1. Open **Settings** > **Notifications**.
|
||||
2. Enable notifications.
|
||||
3. Add one Apprise target URL per line.
|
||||
4. Save and use **Send Test** to verify delivery.
|
||||
|
||||
## Configuration Hierarchy
|
||||
|
||||
@@ -192,6 +185,6 @@ DMARQ validates your configuration on startup. If there are issues, they will be
|
||||
- Database connection parameters
|
||||
- Secret key presence and strength
|
||||
- IMAP credentials (if IMAP is enabled)
|
||||
- SMTP credentials (if alerting is enabled)
|
||||
- Notification targets can be tested from the settings page
|
||||
|
||||
Check the application logs if you encounter startup issues related to configuration.
|
||||
|
||||
@@ -83,7 +83,7 @@ Follow-up:
|
||||
|
||||
## Later Milestones
|
||||
|
||||
- Notifications and alert rules with Apprise.
|
||||
- Notifications and alert rules. Apprise delivery and test notifications are in place; alert rules, summaries, and alert history remain.
|
||||
- DNS health and Cloudflare read-only inspection.
|
||||
- Guided setup and operator health screens.
|
||||
- Forensic/RUF report support.
|
||||
|
||||
+6
-3
@@ -107,15 +107,18 @@ Exit criteria:
|
||||
|
||||
## Milestone 7: Notifications and Alert Rules
|
||||
|
||||
Status: Planned
|
||||
Status: In progress
|
||||
|
||||
Goal: notify administrators when action is needed.
|
||||
|
||||
Delivered:
|
||||
- Apprise notification integration for newline-separated notification target URLs.
|
||||
- Notification settings UI can save Apprise targets, keeps target URLs redacted after save, and can send a test notification.
|
||||
|
||||
Planned:
|
||||
- Apprise notification integration.
|
||||
- Alert rules for new sender source, compliance drop, DMARC failures above threshold, and missing reports.
|
||||
- Daily/weekly summary notifications.
|
||||
- Alert history and test-notification UI.
|
||||
- Alert history.
|
||||
|
||||
Exit criteria:
|
||||
- A user can receive meaningful alerts without opening the dashboard daily.
|
||||
|
||||
+4
-1
@@ -164,7 +164,10 @@ Status: Complete for the delivered reporting milestone. Alert-specific dashboard
|
||||
- [x] Add startup checks for production-critical configuration
|
||||
- [x] Add backup/restore guidance for database deployments
|
||||
- [x] Add release checklist covering migrations, tests, and smoke checks
|
||||
- [ ] Apprise notifications and alert rules
|
||||
- [x] Add Apprise notification delivery and test notification support
|
||||
- [ ] Add alert rules for new sender source, compliance drop, DMARC failures above threshold, and missing reports
|
||||
- [ ] Add daily and weekly summary notifications
|
||||
- [ ] Add alert history
|
||||
- [ ] DNS health guidance and Cloudflare read-only inspection
|
||||
- [ ] Guided setup and operator health pages
|
||||
- [ ] Forensic/RUF report support
|
||||
|
||||
+14
-13
@@ -29,15 +29,18 @@ System-wide settings are available to administrators:
|
||||
|
||||
## Notification Settings
|
||||
|
||||
### Email Notifications
|
||||
### Apprise Notifications
|
||||
|
||||
Configure how you receive email notifications:
|
||||
Configure where DMARQ sends notifications:
|
||||
|
||||
1. Navigate to **Settings** > **Notifications** > **Email**
|
||||
2. Configure the following:
|
||||
- **Email Address**: Where notifications will be sent
|
||||
- **Notification Frequency**: Immediate, daily digest, or weekly summary
|
||||
- **Notification Types**: Select which events trigger notifications
|
||||
1. Navigate to **Settings**.
|
||||
2. Open **Notifications**.
|
||||
3. Enable notifications.
|
||||
4. Add one Apprise target URL per line.
|
||||
5. Save and use **Send Test** to verify delivery.
|
||||
|
||||
Target URLs are redacted after saving so credentials are not exposed through the
|
||||
settings API or page reloads.
|
||||
|
||||
### Alert Thresholds
|
||||
|
||||
@@ -52,11 +55,9 @@ Set thresholds for when alerts are triggered:
|
||||
|
||||
### Integration Notifications
|
||||
|
||||
If you've enabled additional notification channels through Apprise:
|
||||
|
||||
1. Navigate to **Settings** > **Notifications** > **Integrations**
|
||||
2. Configure each integration separately (Slack, Teams, Discord, etc.)
|
||||
3. Set which notification types go to each channel
|
||||
Apprise supports email, Slack, Teams, Discord, generic webhooks, and many other
|
||||
targets through the same notification field. Add each destination on a separate
|
||||
line.
|
||||
|
||||
## API Access
|
||||
|
||||
@@ -139,4 +140,4 @@ Advanced configuration options (administrators only):
|
||||
- **Database Connection**: Change database settings
|
||||
- **Worker Configuration**: Configure background processing settings
|
||||
- **Caching**: Adjust cache settings for performance
|
||||
- **Debug Mode**: Enable additional logging for troubleshooting
|
||||
- **Debug Mode**: Enable additional logging for troubleshooting
|
||||
|
||||
Reference in New Issue
Block a user