#!/usr/bin/env python """ News Sender Service - Standalone microservice for sending newsletters Fetches articles from MongoDB and sends to subscribers via email """ import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime from pathlib import Path from jinja2 import Template from pymongo import MongoClient import os from dotenv import load_dotenv # Load environment variables from backend/.env backend_dir = Path(__file__).parent.parent / 'backend' env_path = backend_dir / '.env' if env_path.exists(): load_dotenv(dotenv_path=env_path) print(f"โ Loaded configuration from: {env_path}") else: print(f"โ Warning: .env file not found at {env_path}") class Config: """Configuration for news sender""" # MongoDB MONGODB_URI = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/') DB_NAME = 'munich_news' # Email SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.gmail.com') SMTP_PORT = int(os.getenv('SMTP_PORT', '587')) EMAIL_USER = os.getenv('EMAIL_USER', '') EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD', '') # Newsletter MAX_ARTICLES = int(os.getenv('NEWSLETTER_MAX_ARTICLES', '10')) WEBSITE_URL = os.getenv('WEBSITE_URL', 'http://localhost:3000') # MongoDB connection client = MongoClient(Config.MONGODB_URI) db = client[Config.DB_NAME] articles_collection = db['articles'] subscribers_collection = db['subscribers'] def get_latest_articles(max_articles=10): """ Get latest articles with AI summaries from database Returns: list: Articles with summaries """ cursor = articles_collection.find( {'summary': {'$exists': True, '$ne': None}} ).sort('created_at', -1).limit(max_articles) articles = [] for doc in cursor: articles.append({ 'title': doc.get('title', ''), 'author': doc.get('author'), 'link': doc.get('link', ''), 'summary': doc.get('summary', ''), 'source': doc.get('source', ''), 'published_at': doc.get('published_at', '') }) return articles def get_active_subscribers(): """ Get all active subscribers from database Returns: list: Email addresses of active subscribers """ cursor = subscribers_collection.find({'status': 'active'}) return [doc['email'] for doc in cursor] def render_newsletter_html(articles): """ Render newsletter HTML from template Args: articles: List of article dictionaries Returns: str: Rendered HTML content """ # Load template template_path = Path(__file__).parent / 'newsletter_template.html' with open(template_path, 'r', encoding='utf-8') as f: template_content = f.read() template = Template(template_content) # Prepare template data now = datetime.now() template_data = { 'date': now.strftime('%A, %B %d, %Y'), 'year': now.year, 'article_count': len(articles), 'articles': articles, 'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe', 'website_link': Config.WEBSITE_URL } # Render HTML return template.render(**template_data) def send_email(to_email, subject, html_content): """ Send email to a single recipient Args: to_email: Recipient email address subject: Email subject html_content: HTML content of email Returns: tuple: (success: bool, error: str or None) """ try: msg = MIMEMultipart('alternative') msg['Subject'] = subject msg['From'] = f'Munich News Daily <{Config.EMAIL_USER}>' msg['To'] = to_email msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z') msg['Message-ID'] = f'<{datetime.now().timestamp()}.{to_email}@dongho.kim>' msg['X-Mailer'] = 'Munich News Daily Sender' # Add plain text version as fallback plain_text = "This email requires HTML support. Please view it in an HTML-capable email client." msg.attach(MIMEText(plain_text, 'plain', 'utf-8')) # Add HTML version msg.attach(MIMEText(html_content, 'html', 'utf-8')) server = smtplib.SMTP(Config.SMTP_SERVER, Config.SMTP_PORT) server.starttls() server.login(Config.EMAIL_USER, Config.EMAIL_PASSWORD) server.send_message(msg) server.quit() return True, None except Exception as e: return False, str(e) def send_newsletter(max_articles=None, test_email=None): """ Send newsletter to all active subscribers Args: max_articles: Maximum number of articles to include (default from config) test_email: If provided, send only to this email (for testing) Returns: dict: Statistics about sending """ print("\n" + "="*70) print("๐ง Munich News Daily - Newsletter Sender") print("="*70) # Validate email configuration if not Config.EMAIL_USER or not Config.EMAIL_PASSWORD: print("โ Email credentials not configured") print(" Set EMAIL_USER and EMAIL_PASSWORD in .env file") return { 'success': False, 'error': 'Email credentials not configured' } # Get articles max_articles = max_articles or Config.MAX_ARTICLES print(f"\nFetching latest {max_articles} articles with AI summaries...") articles = get_latest_articles(max_articles) if not articles: print("โ No articles with summaries found") print(" Run the crawler with Ollama enabled first") return { 'success': False, 'error': 'No articles with summaries' } print(f"โ Found {len(articles)} articles") # Get subscribers if test_email: subscribers = [test_email] print(f"\n๐งช Test mode: Sending to {test_email} only") else: print("\nFetching active subscribers...") subscribers = get_active_subscribers() print(f"โ Found {len(subscribers)} active subscriber(s)") if not subscribers: print("โ No active subscribers found") return { 'success': False, 'error': 'No active subscribers' } # Render newsletter print("\nRendering newsletter HTML...") html_content = render_newsletter_html(articles) print("โ Newsletter rendered") # Send to subscribers subject = f"Munich News Daily - {datetime.now().strftime('%B %d, %Y')}" print(f"\nSending newsletter: '{subject}'") print("-" * 70) sent_count = 0 failed_count = 0 errors = [] for i, email in enumerate(subscribers, 1): print(f"[{i}/{len(subscribers)}] Sending to {email}...", end=' ') success, error = send_email(email, subject, html_content) if success: print("โ") sent_count += 1 else: print(f"โ {error}") failed_count += 1 errors.append({'email': email, 'error': error}) # Summary print("\n" + "="*70) print("๐ Sending Complete") print("="*70) print(f"โ Successfully sent: {sent_count}") print(f"โ Failed: {failed_count}") print(f"๐ฐ Articles included: {len(articles)}") print("="*70 + "\n") return { 'success': True, 'sent_count': sent_count, 'failed_count': failed_count, 'total_subscribers': len(subscribers), 'article_count': len(articles), 'errors': errors } def preview_newsletter(max_articles=None): """ Generate newsletter HTML for preview (doesn't send) Args: max_articles: Maximum number of articles to include Returns: str: HTML content """ max_articles = max_articles or Config.MAX_ARTICLES articles = get_latest_articles(max_articles) if not articles: return "
Run the crawler with Ollama enabled first.
" return render_newsletter_html(articles) if __name__ == '__main__': import sys # Parse command line arguments if len(sys.argv) > 1: command = sys.argv[1] if command == 'preview': # Generate preview HTML html = preview_newsletter() output_file = 'newsletter_preview.html' with open(output_file, 'w', encoding='utf-8') as f: f.write(html) print(f"โ Preview saved to {output_file}") print(f" Open it in your browser to see the newsletter") elif command == 'test': # Send test email if len(sys.argv) < 3: print("Usage: python sender_service.py test