email_notifications #7
@@ -14,11 +14,9 @@ server:
|
|||||||
companyname: "99tales Gmbh"
|
companyname: "99tales Gmbh"
|
||||||
res_id_source_context: "99tales"
|
res_id_source_context: "99tales"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: "INFO" # Set to DEBUG for more verbose output
|
level: "INFO" # Set to DEBUG for more verbose output
|
||||||
file: "alpinebits.log" # Log file path, or null for console only
|
file: "alpinebits.log" # Log file path, or null for console only
|
||||||
|
|
||||||
alpine_bits_auth:
|
alpine_bits_auth:
|
||||||
- hotel_id: "39054_001"
|
- hotel_id: "39054_001"
|
||||||
@@ -40,48 +38,48 @@ alpine_bits_auth:
|
|||||||
username: "erika"
|
username: "erika"
|
||||||
password: !secret ERIKA_PASSWORD
|
password: !secret ERIKA_PASSWORD
|
||||||
|
|
||||||
|
api_tokens:
|
||||||
|
- tLTI8wXF1OVEvUX7kdZRhSW3Qr5feBCz0mHo-kbnEp0
|
||||||
|
|
||||||
# Email configuration for monitoring and alerts
|
# Email configuration for monitoring and alerts
|
||||||
email:
|
email:
|
||||||
# SMTP server configuration
|
# SMTP server configuration
|
||||||
smtp:
|
smtp:
|
||||||
host: "smtp.titan.email" # Your SMTP server
|
host: "smtp.titan.email" # Your SMTP server
|
||||||
port: 465 # Usually 587 for TLS, 465 for SSL
|
port: 465 # Usually 587 for TLS, 465 for SSL
|
||||||
username: info@99tales.net # SMTP username
|
username: info@99tales.net # SMTP username
|
||||||
password: !secret EMAIL_PASSWORD # SMTP password
|
password: !secret EMAIL_PASSWORD # SMTP password
|
||||||
use_tls: false # Use STARTTLS
|
use_tls: false # Use STARTTLS
|
||||||
use_ssl: true # Use SSL/TLS from start
|
use_ssl: true # Use SSL/TLS from start
|
||||||
|
|
||||||
# Email addresses
|
# Email addresses
|
||||||
from_address: "info@99tales.net" # Sender address
|
from_address: "info@99tales.net" # Sender address
|
||||||
from_name: "AlpineBits Monitor" # Sender display name
|
from_name: "AlpineBits Monitor" # Sender display name
|
||||||
|
|
||||||
api_tokens:
|
|
||||||
- tLTI8wXF1OVEvUX7kdZRhSW3Qr5feBCz0mHo-kbnEp0
|
|
||||||
|
|
||||||
# Monitoring and alerting
|
# Monitoring and alerting
|
||||||
monitoring:
|
monitoring:
|
||||||
# Daily report configuration
|
# Daily report configuration
|
||||||
daily_report:
|
daily_report:
|
||||||
enabled: false # Set to true to enable daily reports
|
enabled: false # Set to true to enable daily reports
|
||||||
recipients:
|
recipients:
|
||||||
- "jonas@vaius.ai"
|
- "jonas@vaius.ai"
|
||||||
#- "dev@99tales.com"
|
#- "dev@99tales.com"
|
||||||
send_time: "08:00" # Time to send daily report (24h format, local time)
|
send_time: "08:00" # Time to send daily report (24h format, local time)
|
||||||
include_stats: true # Include reservation/customer stats
|
include_stats: true # Include reservation/customer stats
|
||||||
include_errors: true # Include error summary
|
include_errors: true # Include error summary
|
||||||
|
|
||||||
# Error alert configuration (hybrid approach)
|
# Error alert configuration (hybrid approach)
|
||||||
error_alerts:
|
error_alerts:
|
||||||
enabled: false # Set to true to enable error alerts
|
enabled: false # Set to true to enable error alerts
|
||||||
recipients:
|
recipients:
|
||||||
- "jonas@vaius.ai"
|
- "jonas@vaius.ai"
|
||||||
#- "oncall@99tales.com"
|
#- "oncall@99tales.com"
|
||||||
# Alert is sent immediately if threshold is reached
|
# Alert is sent immediately if threshold is reached
|
||||||
error_threshold: 5 # Send immediate alert after N errors
|
error_threshold: 5 # Send immediate alert after N errors
|
||||||
# Otherwise, alert is sent after buffer time expires
|
# Otherwise, alert is sent after buffer time expires
|
||||||
buffer_minutes: 15 # Wait N minutes before sending buffered errors
|
buffer_minutes: 15 # Wait N minutes before sending buffered errors
|
||||||
# Cooldown period to prevent alert spam
|
# Cooldown period to prevent alert spam
|
||||||
cooldown_minutes: 15 # Wait N min before sending another alert
|
cooldown_minutes: 15 # Wait N min before sending another alert
|
||||||
# Error severity levels to monitor
|
# Error severity levels to monitor
|
||||||
log_levels:
|
log_levels:
|
||||||
- "ERROR"
|
- "ERROR"
|
||||||
|
|||||||
@@ -299,10 +299,11 @@ async def lifespan(app: FastAPI):
|
|||||||
report_scheduler.set_stats_collector(stats_collector.collect_stats)
|
report_scheduler.set_stats_collector(stats_collector.collect_stats)
|
||||||
_LOGGER.info("Stats collector initialized and hooked up to report scheduler")
|
_LOGGER.info("Stats collector initialized and hooked up to report scheduler")
|
||||||
|
|
||||||
# Send a test daily report on startup for testing
|
# Send a test daily report on startup for testing (with 24-hour lookback)
|
||||||
_LOGGER.info("Sending test daily report on startup")
|
_LOGGER.info("Sending test daily report on startup (last 24 hours)")
|
||||||
try:
|
try:
|
||||||
stats = await stats_collector.collect_stats()
|
# Use lookback_hours=24 to get stats from last 24 hours
|
||||||
|
stats = await stats_collector.collect_stats(lookback_hours=24)
|
||||||
success = await email_service.send_daily_report(
|
success = await email_service.send_daily_report(
|
||||||
recipients=report_scheduler.recipients,
|
recipients=report_scheduler.recipients,
|
||||||
stats=stats,
|
stats=stats,
|
||||||
|
|||||||
@@ -492,16 +492,27 @@ class ReservationStatsCollector:
|
|||||||
len(self._hotel_map),
|
len(self._hotel_map),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def collect_stats(self) -> dict[str, Any]:
|
async def collect_stats(self, lookback_hours: int | None = None) -> dict[str, Any]:
|
||||||
"""Collect reservation statistics for the reporting period.
|
"""Collect reservation statistics for the reporting period.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lookback_hours: Optional override to look back N hours from now.
|
||||||
|
If None, uses time since last report.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with statistics including reservations per hotel
|
Dictionary with statistics including reservations per hotel
|
||||||
|
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
period_start = self._last_report_time
|
|
||||||
period_end = now
|
if lookback_hours is not None:
|
||||||
|
# Override mode: look back N hours from now
|
||||||
|
period_start = now - timedelta(hours=lookback_hours)
|
||||||
|
period_end = now
|
||||||
|
else:
|
||||||
|
# Normal mode: since last report
|
||||||
|
period_start = self._last_report_time
|
||||||
|
period_end = now
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Collecting reservation stats from %s to %s",
|
"Collecting reservation stats from %s to %s",
|
||||||
@@ -538,8 +549,9 @@ class ReservationStatsCollector:
|
|||||||
# Sort by reservation count descending
|
# Sort by reservation count descending
|
||||||
hotels_stats.sort(key=lambda x: x["reservations"], reverse=True)
|
hotels_stats.sort(key=lambda x: x["reservations"], reverse=True)
|
||||||
|
|
||||||
# Update last report time
|
# Update last report time only in normal mode (not lookback mode)
|
||||||
self._last_report_time = now
|
if lookback_hours is None:
|
||||||
|
self._last_report_time = now
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
"reporting_period": {
|
"reporting_period": {
|
||||||
|
|||||||
294
test_smtp.py
Normal file
294
test_smtp.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test script to diagnose SMTP connection issues.
|
||||||
|
|
||||||
|
This script tests SMTP connectivity with different configurations to help
|
||||||
|
identify whether the issue is with credentials, network, ports, or TLS settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import smtplib
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
# Load configuration from config.yaml
|
||||||
|
try:
|
||||||
|
from alpine_bits_python.config_loader import load_config
|
||||||
|
|
||||||
|
print("Loading configuration from config.yaml...")
|
||||||
|
config = load_config()
|
||||||
|
email_config = config.get("email", {})
|
||||||
|
smtp_config = email_config.get("smtp", {})
|
||||||
|
|
||||||
|
SMTP_HOST = smtp_config.get("host", "smtp.titan.email")
|
||||||
|
SMTP_PORT = smtp_config.get("port", 465)
|
||||||
|
SMTP_USERNAME = smtp_config.get("username", "")
|
||||||
|
SMTP_PASSWORD = smtp_config.get("password", "")
|
||||||
|
USE_TLS = smtp_config.get("use_tls", False)
|
||||||
|
USE_SSL = smtp_config.get("use_ssl", True)
|
||||||
|
FROM_ADDRESS = email_config.get("from_address", "info@99tales.net")
|
||||||
|
FROM_NAME = email_config.get("from_name", "AlpineBits Monitor")
|
||||||
|
|
||||||
|
# Get test recipient
|
||||||
|
monitoring_config = email_config.get("monitoring", {})
|
||||||
|
daily_report = monitoring_config.get("daily_report", {})
|
||||||
|
recipients = daily_report.get("recipients", [])
|
||||||
|
TEST_RECIPIENT = recipients[0] if recipients else "jonas@vaius.ai"
|
||||||
|
|
||||||
|
print(f"✓ Configuration loaded successfully")
|
||||||
|
print(f" SMTP Host: {SMTP_HOST}")
|
||||||
|
print(f" SMTP Port: {SMTP_PORT}")
|
||||||
|
print(f" Username: {SMTP_USERNAME}")
|
||||||
|
print(f" Password: {'***' if SMTP_PASSWORD else '(not set)'}")
|
||||||
|
print(f" Use SSL: {USE_SSL}")
|
||||||
|
print(f" Use TLS: {USE_TLS}")
|
||||||
|
print(f" From: {FROM_ADDRESS}")
|
||||||
|
print(f" Test Recipient: {TEST_RECIPIENT}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to load configuration: {e}")
|
||||||
|
print("Using default values for testing...")
|
||||||
|
SMTP_HOST = "smtp.titan.email"
|
||||||
|
SMTP_PORT = 465
|
||||||
|
SMTP_USERNAME = input("Enter SMTP username: ")
|
||||||
|
SMTP_PASSWORD = input("Enter SMTP password: ")
|
||||||
|
USE_TLS = False
|
||||||
|
USE_SSL = True
|
||||||
|
FROM_ADDRESS = "info@99tales.net"
|
||||||
|
FROM_NAME = "AlpineBits Monitor"
|
||||||
|
TEST_RECIPIENT = input("Enter test recipient email: ")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_message(subject: str) -> MIMEMultipart:
|
||||||
|
"""Create a test email message."""
|
||||||
|
msg = MIMEMultipart("alternative")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = f"{FROM_NAME} <{FROM_ADDRESS}>"
|
||||||
|
msg["To"] = TEST_RECIPIENT
|
||||||
|
msg["Date"] = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
|
||||||
|
|
||||||
|
body = f"""SMTP Connection Test - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
|
|
||||||
|
This is a test email to verify SMTP connectivity.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
- SMTP Host: {SMTP_HOST}
|
||||||
|
- SMTP Port: {SMTP_PORT}
|
||||||
|
- Use SSL: {USE_SSL}
|
||||||
|
- Use TLS: {USE_TLS}
|
||||||
|
|
||||||
|
If you received this email, the SMTP configuration is working correctly!
|
||||||
|
"""
|
||||||
|
|
||||||
|
msg.attach(MIMEText(body, "plain"))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_smtp_connection(host: str, port: int, timeout: int = 10) -> bool:
|
||||||
|
"""Test basic TCP connection to SMTP server."""
|
||||||
|
import socket
|
||||||
|
|
||||||
|
print(f"Test 1: Testing TCP connection to {host}:{port}...")
|
||||||
|
try:
|
||||||
|
sock = socket.create_connection((host, port), timeout=timeout)
|
||||||
|
sock.close()
|
||||||
|
print(f"✓ TCP connection successful to {host}:{port}")
|
||||||
|
return True
|
||||||
|
except socket.timeout:
|
||||||
|
print(f"✗ Connection timed out after {timeout} seconds")
|
||||||
|
print(f" This suggests a network/firewall issue blocking access to {host}:{port}")
|
||||||
|
return False
|
||||||
|
except socket.error as e:
|
||||||
|
print(f"✗ Connection failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_smtp_ssl(host: str, port: int, username: str, password: str, timeout: int = 30) -> bool:
|
||||||
|
"""Test SMTP connection with SSL."""
|
||||||
|
print(f"\nTest 2: Testing SMTP with SSL (port {port})...")
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
with smtplib.SMTP_SSL(host, port, timeout=timeout, context=context) as server:
|
||||||
|
print(f"✓ Connected to SMTP server with SSL")
|
||||||
|
|
||||||
|
# Try to get server info
|
||||||
|
server.ehlo()
|
||||||
|
print(f"✓ EHLO successful")
|
||||||
|
|
||||||
|
# Try authentication if credentials provided
|
||||||
|
if username and password:
|
||||||
|
print(f" Attempting authentication as: {username}")
|
||||||
|
server.login(username, password)
|
||||||
|
print(f"✓ Authentication successful")
|
||||||
|
else:
|
||||||
|
print(f"⚠ No credentials provided, skipping authentication")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except smtplib.SMTPAuthenticationError as e:
|
||||||
|
print(f"✗ Authentication failed: {e}")
|
||||||
|
print(f" Check your username and password")
|
||||||
|
return False
|
||||||
|
except socket.timeout:
|
||||||
|
print(f"✗ Connection timed out after {timeout} seconds")
|
||||||
|
print(f" Try increasing timeout or check network/firewall")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ SMTP SSL failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_smtp_tls(host: str, port: int, username: str, password: str, timeout: int = 30) -> bool:
|
||||||
|
"""Test SMTP connection with STARTTLS."""
|
||||||
|
print(f"\nTest 3: Testing SMTP with STARTTLS (port {port})...")
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP(host, port, timeout=timeout) as server:
|
||||||
|
print(f"✓ Connected to SMTP server")
|
||||||
|
|
||||||
|
# Try STARTTLS
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
server.starttls(context=context)
|
||||||
|
print(f"✓ STARTTLS successful")
|
||||||
|
|
||||||
|
# Try authentication if credentials provided
|
||||||
|
if username and password:
|
||||||
|
print(f" Attempting authentication as: {username}")
|
||||||
|
server.login(username, password)
|
||||||
|
print(f"✓ Authentication successful")
|
||||||
|
else:
|
||||||
|
print(f"⚠ No credentials provided, skipping authentication")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except smtplib.SMTPAuthenticationError as e:
|
||||||
|
print(f"✗ Authentication failed: {e}")
|
||||||
|
return False
|
||||||
|
except socket.timeout:
|
||||||
|
print(f"✗ Connection timed out after {timeout} seconds")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ SMTP TLS failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def send_test_email(host: str, port: int, username: str, password: str,
|
||||||
|
use_ssl: bool, use_tls: bool, timeout: int = 30) -> bool:
|
||||||
|
"""Send an actual test email."""
|
||||||
|
print(f"\nTest 4: Sending test email...")
|
||||||
|
try:
|
||||||
|
msg = create_test_message("SMTP Test Email - AlpineBits")
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
with smtplib.SMTP_SSL(host, port, timeout=timeout, context=context) as server:
|
||||||
|
if username and password:
|
||||||
|
server.login(username, password)
|
||||||
|
server.send_message(msg, FROM_ADDRESS, [TEST_RECIPIENT])
|
||||||
|
|
||||||
|
else:
|
||||||
|
with smtplib.SMTP(host, port, timeout=timeout) as server:
|
||||||
|
if use_tls:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
server.starttls(context=context)
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
server.login(username, password)
|
||||||
|
|
||||||
|
server.send_message(msg, FROM_ADDRESS, [TEST_RECIPIENT])
|
||||||
|
|
||||||
|
print(f"✓ Test email sent successfully to {TEST_RECIPIENT}")
|
||||||
|
print(f" Check your inbox!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ Failed to send email: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run all SMTP tests."""
|
||||||
|
print("=" * 70)
|
||||||
|
print("SMTP Connection Test Script")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 1: Basic TCP connection
|
||||||
|
tcp_ok = test_smtp_connection(SMTP_HOST, SMTP_PORT, timeout=10)
|
||||||
|
|
||||||
|
if not tcp_ok:
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("DIAGNOSIS: Cannot establish TCP connection to SMTP server")
|
||||||
|
print("=" * 70)
|
||||||
|
print("\nPossible causes:")
|
||||||
|
print("1. The SMTP server is down or unreachable")
|
||||||
|
print("2. A firewall is blocking the connection")
|
||||||
|
print("3. The host or port is incorrect")
|
||||||
|
print("4. Network connectivity issues from your container/server")
|
||||||
|
print("\nTroubleshooting:")
|
||||||
|
print(f"- Verify the server is correct: {SMTP_HOST}")
|
||||||
|
print(f"- Verify the port is correct: {SMTP_PORT}")
|
||||||
|
print("- Check if your container/server has outbound internet access")
|
||||||
|
print("- Try from a different network or machine")
|
||||||
|
print(f"- Use telnet/nc to test: telnet {SMTP_HOST} {SMTP_PORT}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Test 2 & 3: Try both SSL and TLS
|
||||||
|
ssl_ok = False
|
||||||
|
tls_ok = False
|
||||||
|
|
||||||
|
if USE_SSL:
|
||||||
|
ssl_ok = test_smtp_ssl(SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, timeout=30)
|
||||||
|
|
||||||
|
# Also try common alternative ports
|
||||||
|
if not ssl_ok and SMTP_PORT == 465:
|
||||||
|
print("\n⚠ Port 465 failed, trying port 587 with STARTTLS...")
|
||||||
|
tls_ok = test_smtp_tls(SMTP_HOST, 587, SMTP_USERNAME, SMTP_PASSWORD, timeout=30)
|
||||||
|
|
||||||
|
if USE_TLS:
|
||||||
|
tls_ok = test_smtp_tls(SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, timeout=30)
|
||||||
|
|
||||||
|
if not ssl_ok and not tls_ok:
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("DIAGNOSIS: Cannot authenticate or establish secure connection")
|
||||||
|
print("=" * 70)
|
||||||
|
print("\nPossible causes:")
|
||||||
|
print("1. Wrong username or password")
|
||||||
|
print("2. Wrong port for the encryption method")
|
||||||
|
print("3. SSL/TLS version mismatch")
|
||||||
|
print("\nTroubleshooting:")
|
||||||
|
print("- Verify your credentials are correct")
|
||||||
|
print("- Port 465 typically uses SSL")
|
||||||
|
print("- Port 587 typically uses STARTTLS")
|
||||||
|
print("- Port 25 is usually unencrypted (not recommended)")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Test 4: Send actual email
|
||||||
|
send_ok = send_test_email(
|
||||||
|
SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD,
|
||||||
|
USE_SSL, USE_TLS, timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
if send_ok:
|
||||||
|
print("✓ ALL TESTS PASSED!")
|
||||||
|
print("=" * 70)
|
||||||
|
print("\nYour SMTP configuration is working correctly.")
|
||||||
|
print(f"Check {TEST_RECIPIENT} for the test email.")
|
||||||
|
else:
|
||||||
|
print("⚠ PARTIAL SUCCESS")
|
||||||
|
print("=" * 70)
|
||||||
|
print("\nConnection and authentication work, but email sending failed.")
|
||||||
|
print("This might be a temporary issue. Try again.")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nTest cancelled by user")
|
||||||
|
sys.exit(1)
|
||||||
Reference in New Issue
Block a user