Commit 8352436d authored by Anthony Jacob's avatar Anthony Jacob
Browse files

test email feature

parent 95652f66
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
import os
from stat import FILE_ATTRIBUTE_DIRECTORY
from dotenv import load_dotenv
from datetime import timedelta

@@ -27,6 +28,7 @@ class Config:
    POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")

    LOGO_DIR = os.getenv("LOGO_DIR")
    FILE_DIR = os.getenv("FILE_DIR")

    # Microsoft Graph API Configuration
    AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
+73 −0
Original line number Diff line number Diff line
import os
from flask import Blueprint, request, jsonify, current_app
from helpers.email import send_email, send_email_with_attachment
from helpers.security import require_auth
from helpers.limiter import limiter
import re
from email_validator import validate_email, EmailNotValidError


email_bp = Blueprint("email", __name__)


@email_bp.route("/email", methods=["POST"])
@require_auth
@limiter.limit("1/second", override_defaults=False)
def sendmail():
    data = request.get_json()
    if "email" not in data or not isinstance(data["email"], str):
        return jsonify({"error": "Invalid or missing email field"}), 400

    try:
        validate_email(data["email"])
    except EmailNotValidError as e:
        return jsonify({"error": str(e)}), 400

    if "subject" not in data:
        return jsonify({"error": "missing subject"}), 400

    if "body" not in data:
        return jsonify({"error": "missing body"}), 400

    if "is_html" in data and not isinstance(data["is_html"], bool):
        return jsonify({"error": "is_html must be a boolean"}), 400
    elif "is_html" in data and isinstance(data["is_html"], bool):
        is_html = data["is_html"]
    elif "is_html" not in data:
        is_html = False


    email: str = data["email"]
    subject: str = data["subject"]
    body: str = data["body"]

    if( "attachment" in data and "attachment_name" in data):
        try:
            # Read the PDF file content
            file_path =  os.path.join(
                        current_app.config["FILE_DIR"], data["attachment"]
                    )

            with open(file_path, "rb") as file:
                file_content = file.read()

            response = send_email_with_attachment(
                recipient_email=email,
                subject=subject,
                body=body,
                is_html=is_html,
                attachment_name=data["attachment_name"],
                attachment_content=file_content
            )


        except FileNotFoundError:
            return jsonify({"error": "PDF file not found"}), 400
        except Exception as e:
            return jsonify({"error": str(e)}), 500
    else:
        response = send_email(recipient_email=email, subject=subject, body=body, is_html=is_html)


    return jsonify({"response": f"{response.status_code} - {response.text}"}), 200

app/helpers/email.py

0 → 100644
+139 −0
Original line number Diff line number Diff line
import base64
from os import access
import requests
from flask import current_app

def send_email(recipient_email: str, subject: str, body: str, is_html=False) -> None | requests.Response:
    """
    Sends an email using the Microsoft Graph API with application permissions.

    :param recipient_email: Recipient's email address
    :param subject: Subject of the email
    :param body: Body of the email
    :param is_html: If True, the body is treated as HTML
    :return: Response from the Graph API
    """
    access_token = get_access_token()
    if not access_token:
        return None

    # Use users endpoint instead of /me
    graph_api_url = f"{current_app.config['AZURE_GRAPH_API_ENDPOINT']}/users/{current_app.config['EMAIL_SENDER_ADDRESS']}/sendMail"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    email_data = {
        "message": {
            "subject": subject,
            "body": {
                "contentType": "HTML" if is_html else "Text",
                "content": body
            },
            "toRecipients": [
                {
                    "emailAddress": {
                        "address": recipient_email
                    }
                }
            ]
        },
        "saveToSentItems": True
    }

    response = requests.post(graph_api_url, headers=headers, json=email_data, )
    if response.status_code != 202:
        current_app.logger.error(f"Failed to send email: {response.status_code} - {response.text}")
    return response

def send_email_with_attachment(
    recipient_email: str,
    subject: str,
    body: str,
    attachment_name: str,
    attachment_content: bytes,
    is_html: bool = False
) -> None | requests.Response:
    """
    Sends an email with an attachment using the Microsoft Graph API with application permissions.

    :param recipient_email: Recipient's email address
    :param subject: Subject of the email
    :param body: Body of the email
    :param attachment_name: Name of the attachment file
    :param attachment_content: Content of the attachment as a byte string
    :param is_html: If True, the body is treated as HTML
    :return: Response from the Graph API
    """
    access_token = get_access_token()
    if not access_token:
        return None

    graph_api_url = f"{current_app.config['AZURE_GRAPH_API_ENDPOINT']}/users/{current_app.config['EMAIL_SENDER_ADDRESS']}/sendMail"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    email_data = {
        "message": {
            "subject": subject,
            "body": {
                "contentType": "HTML" if is_html else "Text",
                "content": body
            },
            "toRecipients": [
                {
                    "emailAddress": {
                        "address": recipient_email
                    }
                }
            ],
            "attachments": [
                {
                    "@odata.type": "#microsoft.graph.fileAttachment",
                    "name": attachment_name,
                    "contentBytes": base64.b64encode(attachment_content).decode('utf-8')
                },
                {
                    "@odata.type": "#microsoft.graph.fileAttachment",
                    "name": attachment_name + "2.pdf",
                    "contentBytes": base64.b64encode(attachment_content).decode('utf-8')
                }
            ]
        },
        "saveToSentItems": True
    }

    response = requests.post(graph_api_url, headers=headers, json=email_data)
    if response.status_code != 202:
        current_app.logger.error(f"Failed to send email with attachment: {response.status_code} - {response.text}")
    return response


def get_access_token():
    """
    Retrieves an Azure AD access token using client credentials flow.

    :return: Access token as a string
    """
    client_id = current_app.config['AZURE_CLIENT_ID']
    client_secret = current_app.config['AZURE_CLIENT_SECRET']
    tenant_id = current_app.config['AZURE_TENANT_ID']

    token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }
    data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": "https://graph.microsoft.com/.default"
    }

    response = requests.post(token_url, headers=headers, data=data)
    if response.status_code == 200:
        return response.json().get("access_token")
    else:
        current_app.logger.error(f"Failed to retrieve access token: {response.status_code} - {response.text}")
        return None
 No newline at end of file
+0 −0

File moved.

+0 −0

Empty file added.

Loading