migrate from gotify to ntfy with image attachment support

- 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>
This commit is contained in:
2025-12-02 18:59:23 -06:00
parent 5ae4104e24
commit da1467cc10
5 changed files with 221 additions and 19 deletions

View File

@@ -5,5 +5,5 @@ load_dotenv()
from main import handle_hook
msg_id = "jtdx9og7NAQ6LuVvEmRL6t"
msg_id = "42EJJktKsUUaH7zjQ6YoKY"
handle_hook(msg_id)

View File

@@ -24,8 +24,11 @@ load_dotenv()
MAILPIT_API = os.getenv("MAILPIT_API", "http://localhost:8025")
MAILPIT_TOKEN = os.getenv("MAILPIT_TOKEN", "")
GOTIFY_URL = os.getenv("GOTIFY_URL","")
GOTIFY_TOKEN = os.getenv("GOTIFY_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(
@@ -36,7 +39,7 @@ log = logging.getLogger("mailpit-hook")
app = Flask(__name__)
from utils.gotify_api import GotifyNotifier
from utils.ntfy_api import NtfyNotifier
# ------------------------------------------------------------------ #
# Helpers
@@ -53,16 +56,46 @@ def get_mailpit_message(message_id: str) -> Dict[str, Any]:
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}"
def send_gotify(title: str, message: str, priority: int = 5) -> None:
"""Send a message to Gotify."""
notify = GotifyNotifier(GOTIFY_URL, GOTIFY_TOKEN)
result = notify.gotify(title=title,markdown=message,priority=5)
if not result:
log.warning("Gotify push failed")
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:
log.info("Gotify push OK")
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
# ------------------------------------------------------------------ #
@@ -97,16 +130,35 @@ def hook():
return jsonify({"error":"Error sending webhook"}), 500
def handle_hook(msg_id:str):
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] + "..."
send_gotify(subject, preview)
# 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:
@@ -119,6 +171,4 @@ def handle_hook(msg_id:str):
# ------------------------------------------------------------------ #
if __name__ == "__main__":
# msg_id = "ZTUUK57e7kUoaviua6TCgP@mailpit"
# msg = get_mailpit_message(msg_id)
app.run(host="0.0.0.0", port=8088)

81
app/utils/ntfy_api.py Normal file
View File

@@ -0,0 +1,81 @@
import requests
class NtfyNotifier:
def __init__(self, server_url: str, api_token: str, topic: str):
self.base_url = server_url.rstrip('/')
self.token = api_token
self.topic = topic
def send(self, title: str = "Test", message: str = "testing msg", priority: int = 5) -> bool:
"""
Send a text message to Ntfy.
Args:
title (str): Title of the notification.
message (str): Body of the notification.
priority (int, optional): Message priority (1-5). Defaults to 5.
Returns:
bool: True if the message was sent successfully, False otherwise.
"""
if not self.base_url or not self.token or not self.topic:
print("[!] Missing NTFY_URL, NTFY_TOKEN, or NTFY_TOPIC")
return False
url = f"{self.base_url}/{self.topic}"
headers = {
"Authorization": f"Bearer {self.token}",
"Title": title,
"Priority": str(priority),
}
try:
response = requests.post(url, headers=headers, data=message, timeout=10)
response.raise_for_status()
return True
except requests.RequestException as e:
print(f"[!] Failed to send Ntfy message: {e}")
return False
def send_with_image(self, title: str, message: str, image_data: bytes,
filename: str = "image.jpg", priority: int = 5) -> bool:
"""
Send a notification with an attached image to Ntfy.
Args:
title (str): Title of the notification.
message (str): Body of the notification.
image_data (bytes): Binary image data to attach.
filename (str): Filename for the attachment. Defaults to "image.jpg".
priority (int, optional): Message priority (1-5). Defaults to 5.
Returns:
bool: True if the message was sent successfully, False otherwise.
"""
if not self.base_url or not self.token or not self.topic:
print("[!] Missing NTFY_URL, NTFY_TOKEN, or NTFY_TOPIC")
return False
url = f"{self.base_url}/{self.topic}"
# Sanitize message for HTTP header (no newlines/carriage returns allowed)
safe_message = message.replace('\r', ' ').replace('\n', ' ').strip()
headers = {
"Authorization": f"Bearer {self.token}",
"Title": title,
"Priority": str(priority),
"Filename": filename,
"Message": safe_message,
}
try:
response = requests.put(url, headers=headers, data=image_data, timeout=10)
response.raise_for_status()
return True
except requests.RequestException as e:
print(f"[!] Failed to send Ntfy message with image: {e}")
return False