314 lines
9.5 KiB
Python
314 lines
9.5 KiB
Python
#!/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 "<h1>No articles with summaries found</h1><p>Run the crawler with Ollama enabled first.</p>"
|
|
|
|
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 <email>")
|
|
sys.exit(1)
|
|
test_email = sys.argv[2]
|
|
send_newsletter(test_email=test_email)
|
|
|
|
elif command == 'send':
|
|
# Send to all subscribers
|
|
max_articles = int(sys.argv[2]) if len(sys.argv) > 2 else None
|
|
send_newsletter(max_articles=max_articles)
|
|
|
|
else:
|
|
print("Unknown command. Usage:")
|
|
print(" python sender_service.py preview - Generate HTML preview")
|
|
print(" python sender_service.py test <email> - Send test email")
|
|
print(" python sender_service.py send [count] - Send to all subscribers")
|
|
else:
|
|
# Default: send newsletter
|
|
send_newsletter()
|