- Replace GotifyNotifier with NtfyNotifier for push notifications - Add support for sending email image attachments to ntfy - Add NTFY_URL, NTFY_TOKEN, NTFY_TOPIC environment variables - Add get_mailpit_image() and get_mailpit_image_thumb() helpers - Sanitize message content for HTTP headers (remove newlines) - Remove all gotify references 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Mailpit → Gotify bridge webhook.
|
|
|
|
Receives POSTs from Mailpit (MP_WEBHOOK_URL) when new mail arrives.
|
|
Fetches the full message from the Mailpit API, extracts useful info,
|
|
and forwards a summary to Gotify.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import json
|
|
from typing import Any, Dict
|
|
|
|
import requests
|
|
from dotenv import load_dotenv
|
|
from flask import Flask, jsonify, request
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Config & logging
|
|
# ------------------------------------------------------------------ #
|
|
|
|
load_dotenv()
|
|
|
|
MAILPIT_API = os.getenv("MAILPIT_API", "http://localhost:8025")
|
|
MAILPIT_TOKEN = os.getenv("MAILPIT_TOKEN", "")
|
|
|
|
NTFY_URL = os.getenv("NTFY_URL", "")
|
|
NTFY_TOKEN = os.getenv("NTFY_TOKEN", "")
|
|
NTFY_TOPIC = os.getenv("NTFY_TOPIC", "")
|
|
|
|
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
|
|
logging.basicConfig(
|
|
level=getattr(logging, LOG_LEVEL, logging.INFO),
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
)
|
|
log = logging.getLogger("mailpit-hook")
|
|
|
|
app = Flask(__name__)
|
|
|
|
from utils.ntfy_api import NtfyNotifier
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Helpers
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def get_mailpit_message(message_id: str) -> Dict[str, Any]:
|
|
"""Retrieve full message JSON from Mailpit REST API."""
|
|
url = f"{MAILPIT_API}/api/v1/message/{message_id}"
|
|
headers = {}
|
|
if MAILPIT_TOKEN:
|
|
headers["Authorization"] = f"Bearer {MAILPIT_TOKEN}"
|
|
|
|
resp = requests.get(url, headers=headers, timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
def get_mailpit_image_thumb(message_id: str, part_id: str) -> bytes:
|
|
"""Retrieve image thumbnail from Mailpit REST API as binary data."""
|
|
url = f"{MAILPIT_API}/api/v1/message/{message_id}/part/{part_id}/thumb"
|
|
headers = {}
|
|
if MAILPIT_TOKEN:
|
|
headers["Authorization"] = f"Bearer {MAILPIT_TOKEN}"
|
|
|
|
resp = requests.get(url, headers=headers, timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.content
|
|
|
|
def get_mailpit_image(message_id: str, part_id: str) -> bytes:
|
|
"""Retrieve image thumbnail from Mailpit REST API as binary data."""
|
|
url = f"{MAILPIT_API}/api/v1/message/{message_id}/part/{part_id}"
|
|
headers = {}
|
|
if MAILPIT_TOKEN:
|
|
headers["Authorization"] = f"Bearer {MAILPIT_TOKEN}"
|
|
|
|
resp = requests.get(url, headers=headers, timeout=10)
|
|
resp.raise_for_status()
|
|
return resp.content
|
|
|
|
def send_notification(title: str, message: str, priority: int = 5,
|
|
image_data: bytes = None, filename: str = "image.jpg") -> bool:
|
|
"""Send a notification to Ntfy, optionally with an image attachment."""
|
|
notify = NtfyNotifier(NTFY_URL, NTFY_TOKEN, NTFY_TOPIC)
|
|
|
|
if image_data:
|
|
result = notify.send_with_image(
|
|
title=title, message=message, image_data=image_data,
|
|
filename=filename, priority=priority
|
|
)
|
|
else:
|
|
result = notify.send(title=title, message=message, priority=priority)
|
|
|
|
if not result:
|
|
log.warning("Ntfy push failed")
|
|
else:
|
|
log.info("Ntfy push OK")
|
|
return result
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Webhook route
|
|
# ------------------------------------------------------------------ #
|
|
|
|
@app.route("/hook", methods=["POST"])
|
|
def hook():
|
|
"""
|
|
Mailpit sends JSON like:
|
|
{
|
|
"ID": "abcdef123",
|
|
"MessageID": "<...>",
|
|
"From": "camera@reolink.local",
|
|
"Subject": "Motion Detected",
|
|
...
|
|
}
|
|
"""
|
|
data = request.get_json(silent=True) or {}
|
|
mail_msg_id = data.get("MessageID")
|
|
msg_id = data.get("ID")
|
|
if not msg_id:
|
|
log.warning("Webhook received malformed payload: %s", data)
|
|
return jsonify({"error": "missing ID"}), 400
|
|
|
|
log.info(f"Webhook triggered for message ID={msg_id} - Email MSG_ID={mail_msg_id}")
|
|
|
|
result = handle_hook(msg_id)
|
|
if result:
|
|
return jsonify({"status": "ok"}), 200
|
|
else:
|
|
return jsonify({"error":"Error sending webhook"}), 500
|
|
|
|
|
|
def handle_hook(msg_id: str):
|
|
try:
|
|
msg = get_mailpit_message(msg_id)
|
|
subject = msg.get("Subject", "(no subject)")
|
|
text = msg.get("Text", "") or msg.get("HTML", "")
|
|
attachments = msg.get("Attachments") or []
|
|
|
|
# Build preview text
|
|
preview = (text or "")
|
|
if len(preview) > 200:
|
|
preview = preview[:200] + "..."
|
|
|
|
# Check for image attachments and fetch thumbnail
|
|
image_data = None
|
|
filename = "image.jpg"
|
|
if attachments:
|
|
first_attachment = attachments[0]
|
|
part_id = first_attachment.get("PartID")
|
|
content_type = first_attachment.get("ContentType", "")
|
|
if part_id and content_type.startswith("image/"):
|
|
try:
|
|
# image_data = get_mailpit_image_thumb(msg_id, part_id)
|
|
image_data = get_mailpit_image(msg_id, part_id)
|
|
# Extract filename from attachment if available
|
|
filename = first_attachment.get("FileName", "image.jpg")
|
|
except Exception as e:
|
|
log.warning("Failed to fetch image thumbnail: %s", e)
|
|
|
|
send_notification(subject, preview, image_data=image_data, filename=filename)
|
|
return True
|
|
|
|
except Exception as e:
|
|
log.exception("Error processing webhook: %s", e)
|
|
return False
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Entry point
|
|
# ------------------------------------------------------------------ #
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8088)
|