Skip to content

Commit

Permalink
Merge pull request #116 from jadmsaadaot/SUBMIT-task#100
Browse files Browse the repository at this point in the history
setup common hosted email service
  • Loading branch information
jadmsaadaot authored Oct 22, 2024
2 parents 1f22254 + 964cff9 commit d20b237
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 0 deletions.
5 changes: 5 additions & 0 deletions submit-api/src/submit_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class DevConfig(_Config): # pylint: disable=too-few-public-methods

TESTING = False
DEBUG = True

CHES_TOKEN_ENDPOINT = os.getenv('CHES_TOKEN_ENDPOINT')
CHES_CLIENT_ID = os.getenv('CHES_CLIENT_ID')
CHES_CLIENT_SECRET = os.getenv('CHES_CLIENT_SECRET')
CHES_BASE_URL = os.getenv('CHES_BASE_URL')
print(f'SQLAlchemy URL (DevConfig): {_Config.SQLALCHEMY_DATABASE_URI}')


Expand Down
2 changes: 2 additions & 0 deletions submit-api/src/submit_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from .submission import API as SUBMISSION_API
from .test import API as TEST_API
from .user import API as USER_API
from .email import API as EMAIL_API


__all__ = ('API_BLUEPRINT', 'OPS_BLUEPRINT')
Expand Down Expand Up @@ -65,3 +66,4 @@
API.add_namespace(ITEM_API)
API.add_namespace(SUBMISSION_API)
API.add_namespace(TEST_API)
API.add_namespace(EMAIL_API)
50 changes: 50 additions & 0 deletions submit-api/src/submit_api/resources/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for managing an email resource."""

from http import HTTPStatus

from flask_restx import Namespace, Resource, cors

from submit_api.utils.util import cors_preflight

from ..auth import auth
from .apihelper import Api as ApiHelper
from ..services.ches_service import ChesApiService, EmailDetails

API = Namespace("email", description="Endpoints for sending emails")
"""Custom exception messages
"""


@cors_preflight("POST, OPTIONS")
@API.route("", methods=["POST", "OPTIONS"])
class Item(Resource):
"""Resource for managing projects."""

@staticmethod
@ApiHelper.swagger_decorators(
API, endpoint_description="send email"
)
@API.response(HTTPStatus.BAD_REQUEST, "Bad Request")
@cors.crossdomain(origin="*")
@auth.require
def post():
"""Send email."""
request_json = API.payload
email_details = EmailDetails(
**request_json
)
response_json, status = ChesApiService().send_email(email_details)
return response_json, status
123 changes: 123 additions & 0 deletions submit-api/src/submit_api/services/ches_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Copyright © 2019 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Service for integrating with the Common Hosted Email Service."""
import base64
import json
from datetime import datetime, timedelta
from typing import Optional, List
import requests
from attr import dataclass
from flask import current_app
from submit_api.utils.template import Template


@dataclass
class EmailDetails:
"""Email details class."""

sender: str
recipients: List[str]
subject: str
body: Optional[str] = None
template_name: Optional[str] = None
body_args: Optional[dict] = None
cc: Optional[List[str]] = None
bcc: Optional[List[str]] = None

def __post_init__(self):
"""Post init method to initialize optional fields."""
self.body_args = self.body_args or {}
self.cc = self.cc or [] # pylint: disable=invalid-name
self.bcc = self.bcc or []


class ChesApiService:
"""CHES api Service class."""

def __init__(self):
"""Initiate class."""
self.token_endpoint = current_app.config.get('CHES_TOKEN_ENDPOINT')
self.service_client_id = current_app.config.get('CHES_CLIENT_ID')
self.service_client_secret = current_app.config.get('CHES_CLIENT_SECRET')
self.ches_base_url = current_app.config.get('CHES_BASE_URL')
self.access_token, self.token_expiry = self._get_access_token()

def _get_access_token(self):
basic_auth_encoded = base64.b64encode(
bytes(f'{self.service_client_id}:{self.service_client_secret}', 'utf-8')
).decode('utf-8')
data = 'grant_type=client_credentials'
response = requests.post(
self.token_endpoint,
data=data,
headers={
'Authorization': f'Basic {basic_auth_encoded}',
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout=10
)
response.raise_for_status()
response_json = response.json()
expires_in = response_json['expires_in']
expiry_time = datetime.now() + timedelta(seconds=expires_in)
return response_json['access_token'], expiry_time

def _ensure_valid_token(self):
if datetime.now() >= self.token_expiry:
self.access_token, self.token_expiry = self._get_access_token()

@staticmethod
def _get_email_body_from_template(template_name: str, body_args: dict):
if not template_name:
raise ValueError('Template name is required')
template = Template.get_template(template_name)
if not template:
raise ValueError('Template not found')
return template.render(body_args)

def _get_email_body(self, email_details: EmailDetails):
if email_details.body:
body = email_details.body
body_type = 'text'
else:
body = self._get_email_body_from_template(email_details.template_name, email_details.body_args)
body_type = 'html'
return body, body_type

def send_email(self, email_details: EmailDetails):
"""Generate document based on template and data."""
self._ensure_valid_token()

body, body_type = self._get_email_body(email_details)
request_body = {
'bodyType': body_type,
'body': body,
'subject': email_details.subject,
'from': email_details.sender,
'to': email_details.recipients,
'cc': email_details.cc,
'bcc': email_details.bcc
}
json_request_body = json.dumps(request_body)
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {self.access_token}'
}
url = f'{self.ches_base_url}/api/v1/email'
response = requests.post(url, data=json_request_body, headers=headers,
timeout=10)
response.raise_for_status()
return response.json(), response.status_code
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<p>Hello {{ name }},</p>
<p>Thank you for submitting the management plan on behalf of {{ certificate_holder_name }} through our new submission tool. We have successfully received your submission, and the details are as follows:</p>
<p><strong>Management Plan Name:</strong> {{ management_plan_name }}</p>
<p><strong>Submission Date:</strong> {{ submission_date }}</p>
<p><strong>Submitted Documents:</strong></p>
<ul>
{% for document in documents %}
<li>{{ document }}</li>
{% endfor %}
</ul>
<p>If you have any questions or require further assistance, please don't hesitate to contact us.</p>
<p>Best regards,</p>
<p>EAO Management Plan Team</p>
16 changes: 16 additions & 0 deletions submit-api/src/submit_api/utils/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Template Services."""

import os
from jinja2 import Environment, FileSystemLoader

templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..', 'templates'))
ENV = Environment(loader=FileSystemLoader(templates_dir), autoescape=True)


class Template:
"""Template helper class."""

@staticmethod
def get_template(template_filename):
"""Get a template from the common template folder."""
return ENV.get_template(template_filename)

0 comments on commit d20b237

Please sign in to comment.