diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..984e2e6 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Email +SENDGRID_API_KEY= +SENDGRID_FROM_EMAIL= \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index aa12ec0..fb3d2f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ pillow ~= 11.0.0 beautifulsoup4 ~= 4.12.3 lxml ~= 5.3.0 openai == 1.52.2 +sendgrid ~= 6.11.0 \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..eb48e1b --- /dev/null +++ b/src/main.py @@ -0,0 +1,11 @@ +import logging + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('app.log') + ] +) \ No newline at end of file diff --git a/src/routers/__init__.py b/src/routers/__init__.py index cf23153..fc77336 100644 --- a/src/routers/__init__.py +++ b/src/routers/__init__.py @@ -6,10 +6,12 @@ from .signals import router as signal_router from .trends import router as trend_router from .users import router as user_router +from .email import router as email_router ALL = [ choice_router, signal_router, trend_router, user_router, + email_router, ] diff --git a/src/routers/email.py b/src/routers/email.py new file mode 100644 index 0000000..21d84dc --- /dev/null +++ b/src/routers/email.py @@ -0,0 +1,65 @@ +""" +Router for email-related endpoints. +""" + +from typing import List +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, EmailStr + +from ..dependencies import require_admin +from ..services.email_service import EmailService +from ..entities import User + +router = APIRouter(prefix="/email", tags=["email"]) + +# Request models +class EmailRequest(BaseModel): + to_emails: List[EmailStr] + subject: str + content: str + content_type: str = "text/plain" + +class NotificationRequest(BaseModel): + to_email: EmailStr + subject: str + template_id: str + dynamic_data: dict + +# Initialize email service +email_service = EmailService() + +@router.post("/send", dependencies=[Depends(require_admin)]) +async def send_email(request: EmailRequest): + """ + Send an email to multiple recipients. + Only accessible by admin users. + """ + success = await email_service.send_email( + to_emails=request.to_emails, + subject=request.subject, + content=request.content, + content_type=request.content_type + ) + + if not success: + raise HTTPException(status_code=500, detail="Failed to send email") + + return {"message": "Email sent successfully"} + +@router.post("/notify", dependencies=[Depends(require_admin)]) +async def send_notification(request: NotificationRequest): + """ + Send a templated notification email. + Only accessible by admin users. + """ + success = await email_service.send_notification_email( + to_email=request.to_email, + subject=request.subject, + template_id=request.template_id, + dynamic_data=request.dynamic_data + ) + + if not success: + raise HTTPException(status_code=500, detail="Failed to send notification") + + return {"message": "Notification sent successfully"} \ No newline at end of file diff --git a/src/services/email_service.py b/src/services/email_service.py new file mode 100644 index 0000000..99b782a --- /dev/null +++ b/src/services/email_service.py @@ -0,0 +1,111 @@ +""" +Email service using SendGrid for sending emails. +""" + +import os +import logging +from typing import List, Optional +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, Email, To, Content, Subject + +logger = logging.getLogger(__name__) + +class EmailService: + """Service class for handling email operations using SendGrid""" + + def __init__(self): + try: + api_key = os.getenv('SENDGRID_API_KEY') + if not api_key: + logger.error("SENDGRID_API_KEY environment variable is not set") + raise ValueError("SendGrid API key is required") + + from_email = os.getenv('SENDGRID_FROM_EMAIL') + if not from_email: + logger.error("SENDGRID_FROM_EMAIL environment variable is not set") + raise ValueError("SendGrid from email is required") + + self.sg_client = SendGridAPIClient(api_key=api_key) + self.from_email = Email(from_email) + logger.info("EmailService initialized successfully") + + except Exception as e: + logger.error(f"Failed to initialize EmailService: {str(e)}") + raise + + async def send_email( + self, + to_emails: List[str], + subject: str, + content: str, + content_type: str = "text/plain" + ) -> bool: + """ + Send an email using SendGrid + """ + try: + logger.info(f"Preparing to send email to {len(to_emails)} recipients") + + # Create personalized emails for each recipient + message = Mail( + from_email=self.from_email, + to_emails=[To(email) for email in to_emails], + subject=Subject(subject), + ) + + # Add content separately + message.content = [Content(content_type, content)] + + logger.debug(f"Email content prepared: subject='{subject}', type='{content_type}'") + + # Send email + response = self.sg_client.send(message) + status_code = response.status_code + + if status_code in [200, 201, 202]: + logger.info(f"Email sent successfully: status_code={status_code}") + return True + else: + logger.error(f"Failed to send email: status_code={status_code}") + return False + + except Exception as e: + logger.error(f"Error sending email: {str(e)}", exc_info=True) + return False + + async def send_notification_email( + self, + to_email: str, + subject: str, + template_id: str, + dynamic_data: dict + ) -> bool: + """ + Send a templated notification email + """ + try: + logger.info(f"Preparing to send notification email to {to_email}") + logger.debug(f"Using template_id: {template_id}") + logger.debug(f"Dynamic data: {dynamic_data}") + + message = Mail( + from_email=self.from_email, + to_emails=[To(to_email)] + ) + + message.template_id = template_id + message.dynamic_template_data = dynamic_data + + response = self.sg_client.send(message) + status_code = response.status_code + + if status_code in [200, 201, 202]: + logger.info(f"Notification email sent successfully: status_code={status_code}") + return True + else: + logger.error(f"Failed to send notification email: status_code={status_code}") + return False + + except Exception as e: + logger.error(f"Error sending notification email: {str(e)}", exc_info=True) + return False \ No newline at end of file