import logging from datetime import datetime, timezone, timedelta from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags from django.conf import settings from django.urls import reverse from monitor.models import NotificationPreference, EmailNotification logger = logging.getLogger(__name__) class EmailService: """Service for sending Bitcoin monitor notifications.""" def __init__(self): self.site_domain = getattr(settings, 'SITE_DOMAIN', 'localhost:8000') self.from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@bitcoin-monitor.com') def get_notification_preferences(self): """Get all active notification preferences.""" return NotificationPreference.objects.filter(is_active=True) def create_email_notification_record(self, recipient, subject, notification_type): """Create a record of the email to be sent.""" return EmailNotification.objects.create( recipient=recipient, subject=subject, notification_type=notification_type, status='pending' ) def update_notification_status(self, email_notification, status, error_message=''): """Update the status of an email notification.""" email_notification.status = status email_notification.error_message = error_message if status == 'sent': email_notification.sent_at = datetime.now(timezone.utc) email_notification.save() def render_email_template(self, template_name, context): """Render email template with context.""" # Add common context variables context.update({ 'dashboard_url': f'http://{self.site_domain}', 'admin_url': f'http://{self.site_domain}/admin/', 'unsubscribe_url': f'http://{self.site_domain}/api/notifications/unsubscribe/', }) # Render HTML and plain text versions html_content = render_to_string(f'emails/{template_name}', context) text_content = strip_tags(html_content) return html_content, text_content def send_event_alert(self, analysis): """Send event alert email.""" try: # Get recipients who want event alerts preferences = self.get_notification_preferences().filter( receive_event_alerts=True ) if not preferences.exists(): logger.info("No recipients for event alerts") return # Calculate percent change percent_change = abs( (float(analysis.current_price) - float(analysis.average_price)) / float(analysis.average_price) * 100 ) # Prepare context context = { 'alert_type': analysis.status, 'current_price': float(analysis.current_price), 'yearly_average': float(analysis.average_price), 'threshold_percent': float(analysis.threshold_percent), 'lower_threshold': float(analysis.lower_threshold), 'upper_threshold': float(analysis.upper_threshold), 'percent_change': percent_change, 'detected_at': analysis.timestamp, 'previous_status': 'neutral', # Could be tracked 'recommendation': self._get_event_recommendation(analysis), } # Send to each recipient for pref in preferences: try: email_notification = self.create_email_notification_record( recipient=pref.email_address, subject=f"🚨 Bitcoin {analysis.status.upper()} Alert: " f"${float(analysis.current_price):.2f}", notification_type='event' ) self._send_single_email( email_notification, 'event_alert.html', context ) except Exception as e: logger.error(f"Failed to send event alert to {pref.email_address}: {e}") logger.info(f"Event alert sent to {preferences.count()} recipients") except Exception as e: logger.error(f"Error sending event alert: {e}") def send_system_alert(self, alert_title, alert_message, severity='warning', affected_component='system', error_code=None): """Send system alert email.""" try: # Get recipients who want system alerts preferences = self.get_notification_preferences().filter( receive_system_alerts=True ) if not preferences.exists(): logger.info("No recipients for system alerts") return # Prepare context context = { 'alert_title': alert_title, 'alert_message': alert_message, 'severity': severity, 'affected_component': affected_component, 'error_code': error_code, 'occurred_at': datetime.now(timezone.utc), 'troubleshooting_steps': self._get_troubleshooting_steps(severity), } # Send to each recipient for pref in preferences: try: email_notification = self.create_email_notification_record( recipient=pref.email_address, subject=f"⚠️ Bitcoin Monitor System Alert: {alert_title}", notification_type='system' ) self._send_single_email( email_notification, 'system_alert.html', context ) except Exception as e: logger.error(f"Failed to send system alert to {pref.email_address}: {e}") logger.info(f"System alert sent to {preferences.count()} recipients") except Exception as e: logger.error(f"Error sending system alert: {e}") def send_daily_digest(self): """Send daily digest email.""" try: # Get recipients who want daily digest preferences = self.get_notification_preferences().filter( receive_daily_digest=True ) if not preferences.exists(): logger.info("No recipients for daily digest") return # Get data for the past 24 hours yesterday = datetime.now(timezone.utc) - timedelta(hours=24) from monitor.models import BitcoinPrice, MarketAnalysis, SystemStatus from django.db.models import Max, Min # Get price stats prices = BitcoinPrice.objects.filter( timestamp__gte=yesterday ) if prices.exists(): current_price = prices.latest('timestamp').price_usd daily_high = prices.aggregate(Max('price_usd'))['price_usd__max'] daily_low = prices.aggregate(Min('price_usd'))['price_usd__min'] daily_change = ((float(daily_high) - float(daily_low)) / float(daily_low) * 100) else: current_price = daily_high = daily_low = 0 daily_change = 0 # Get market status latest_analysis = MarketAnalysis.objects.filter( period='hourly' ).order_by('-timestamp').first() market_status = latest_analysis.status if latest_analysis else 'neutral' # Get event count events_count = MarketAnalysis.objects.filter( is_event=True, timestamp__gte=yesterday ).count() # Get system stats price_fetches = prices.count() successful_fetches = SystemStatus.objects.filter( is_healthy=True, timestamp__gte=yesterday ).count() total_checks = SystemStatus.objects.filter( timestamp__gte=yesterday ).count() uptime_percentage = (successful_fetches / total_checks * 100) if total_checks > 0 else 0 # Get today's events events_today = [] for event in MarketAnalysis.objects.filter( is_event=True, timestamp__gte=yesterday ).order_by('-timestamp')[:5]: events_today.append({ 'type': event.event_type, 'price': float(event.current_price), 'time': event.timestamp, }) # Prepare context context = { 'date': datetime.now(timezone.utc), 'summary_period': 'Last 24 hours', 'market_status': market_status, 'current_price': float(current_price), 'daily_high': float(daily_high) if daily_high else 0, 'daily_low': float(daily_low) if daily_low else 0, 'daily_change': daily_change, 'events_count': events_count, 'price_fetches': price_fetches, 'uptime_percentage': uptime_percentage, 'events_today': events_today, 'market_insight': self._get_daily_market_insight(market_status), } # Send to each recipient for pref in preferences: try: email_notification = self.create_email_notification_record( recipient=pref.email_address, subject=f"📊 Bitcoin Daily Digest - {datetime.now(timezone.utc).strftime('%b %d, %Y')}", notification_type='digest' ) self._send_single_email( email_notification, 'daily_digest.html', context ) except Exception as e: logger.error(f"Failed to send daily digest to {pref.email_address}: {e}") logger.info(f"Daily digest sent to {preferences.count()} recipients") except Exception as e: logger.error(f"Error sending daily digest: {e}") # Try to send system alert about the failure self.send_system_alert( alert_title="Daily Digest Failed", alert_message=f"Failed to send daily digest: {str(e)}", severity='error', affected_component='email_service' ) def send_test_email(self, recipient_email): """Send a test email to verify configuration.""" try: email_notification = self.create_email_notification_record( recipient=recipient_email, subject="✅ Bitcoin Monitor - Test Email", notification_type='test' ) context = { 'test_message': 'This is a test email from Bitcoin Monitor.', 'timestamp': datetime.now(timezone.utc), } self._send_single_email( email_notification, 'system_alert.html', # Reuse system template for test context ) return True, "Test email sent successfully" except Exception as e: logger.error(f"Error sending test email: {e}") return False, str(e) def _send_single_email(self, email_notification, template_name, context): """Send a single email and update notification record.""" try: # Render email content html_content, text_content = self.render_email_template(template_name, context) # Create email email = EmailMultiAlternatives( subject=email_notification.subject, body=text_content, from_email=self.from_email, to=[email_notification.recipient], ) email.attach_alternative(html_content, "text/html") # Send email email.send() # Update notification record email_notification.content_html = html_content email_notification.content_text = text_content self.update_notification_status(email_notification, 'sent') logger.info(f"Email sent to {email_notification.recipient}") except Exception as e: logger.error(f"Failed to send email to {email_notification.recipient}: {e}") email_notification.retry_count += 1 if email_notification.retry_count < 3: self.update_notification_status( email_notification, 'retrying', f"Retry {email_notification.retry_count}/3: {str(e)}" ) else: self.update_notification_status( email_notification, 'failed', f"Failed after 3 retries: {str(e)}" ) raise def _get_event_recommendation(self, analysis): """Get recommendation based on event type.""" if analysis.status == 'dip': return "Price is significantly below yearly average. Consider buying opportunity." elif analysis.status == 'peak': return "Price is significantly above yearly average. Consider taking profits." else: return "Price has returned to normal range. Monitor for further changes." def _get_troubleshooting_steps(self, severity): """Get troubleshooting steps based on severity.""" steps = [ "Check the admin panel for detailed error logs", "Verify internet connection and API access", "Check Redis and Celery worker status", ] if severity == 'critical': steps.append("Restart the monitoring services if necessary") steps.append("Check database connection and disk space") return steps def _get_daily_market_insight(self, market_status): """Get daily market insight based on status.""" insights = { 'dip': "Market shows buying opportunity with prices below yearly average. " "Monitor for further dips before entering.", 'peak': "Market at elevated levels. Consider profit-taking strategies " "and set stop-loss orders.", 'neutral': "Market trading within normal range. Good time to review " "portfolio and set alerts for future movements.", } return insights.get(market_status, "Monitor market for emerging trends.")