Merge pull request #91 from alphagov/send-email-from-templates

Send Email via the API
This commit is contained in:
NIcholas Staples
2016-02-23 13:18:45 +00:00
11 changed files with 405 additions and 91 deletions

View File

@@ -1,5 +1,5 @@
import os
import re
from flask import request, url_for
from flask import Flask, _request_ctx_stack
from flask.ext.sqlalchemy import SQLAlchemy
@@ -92,3 +92,10 @@ def get_db_version():
return full_name.split('_')[0]
except:
return 'n/a'
def email_safe(string):
return "".join([
character.lower() if character.isalnum() or character == "." else ""
for character in re.sub("\s+", ".", string.strip())
])

View File

@@ -33,6 +33,36 @@ def send_sms(service_id, notification_id, encrypted_notification):
current_app.logger.debug(e)
@notify_celery.task(name="send-email")
def send_email(service_id, notification_id, subject, from_address, encrypted_notification):
notification = encryption.decrypt(encrypted_notification)
template = get_model_templates(notification['template'])
try:
notification_db_object = Notification(
id=notification_id,
template_id=notification['template'],
to=notification['to'],
service_id=service_id,
status='sent'
)
save_notification(notification_db_object)
try:
aws_ses_client.send_email(
from_address,
notification['to'],
subject,
template.content
)
except AwsSesClientException as e:
current_app.logger.debug(e)
save_notification(notification_db_object, {"status": "failed"})
except SQLAlchemyError as e:
current_app.logger.debug(e)
@notify_celery.task(name='send-sms-code')
def send_sms_code(encrypted_verification):
verification_message = encryption.decrypt(encrypted_verification)

View File

@@ -3,23 +3,30 @@ import uuid
from flask import (
Blueprint,
jsonify,
request
request,
current_app
)
from app import api_user, encryption
from app.aws_sqs import add_notification_to_queue
from app.dao import (templates_dao, notifications_dao)
from app.dao import (
templates_dao,
users_dao,
services_dao,
notifications_dao
)
from app.schemas import (
email_notification_schema,
sms_template_notification_schema,
notification_status_schema
)
from app.celery.tasks import send_sms
from app.celery.tasks import send_sms, send_email
from sqlalchemy.orm.exc import NoResultFound
notifications = Blueprint('notifications', __name__)
from app.errors import register_errors
register_errors(notifications)
@@ -42,11 +49,20 @@ def create_sms_notification():
if errors:
return jsonify(result="error", message=errors), 400
try:
templates_dao.get_model_templates(template_id=notification['template'], service_id=api_user['client'])
except NoResultFound:
template = templates_dao.dao_get_template_by_id_and_service_id(
template_id=notification['template'],
service_id=api_user['client']
)
if not template:
return jsonify(result="error", message={'template': ['Template not found']}), 400
service = services_dao.dao_fetch_service_by_id(api_user['client'])
if service.restricted:
if notification['to'] not in [user.email_address for user in service.users]:
return jsonify(result="error", message={'to': ['Invalid phone number for restricted service']}), 400
notification_id = create_notification_id()
send_sms.apply_async((
@@ -59,17 +75,38 @@ def create_sms_notification():
@notifications.route('/email', methods=['POST'])
def create_email_notification():
resp_json = request.get_json()
notification, errors = email_notification_schema.load(resp_json)
notification, errors = email_notification_schema.load(request.get_json())
if errors:
return jsonify(result="error", message=errors), 400
notification_id = add_notification_to_queue(api_user['client'], "admin", 'email', notification)
template = templates_dao.dao_get_template_by_id_and_service_id(
template_id=notification['template'],
service_id=api_user['client']
)
if not template:
return jsonify(result="error", message={'template': ['Template not found']}), 400
service = services_dao.dao_fetch_service_by_id(api_user['client'])
if service.restricted:
if notification['to'] not in [user.email_address for user in service.users]:
return jsonify(result="error", message={'to': ['Email address not permitted for restricted service']}), 400
notification_id = create_notification_id()
send_email.apply_async((
api_user['client'],
notification_id,
template.subject,
"{}@{}".format(service.email_from, current_app.config['NOTIFY_EMAIL_DOMAIN']),
encryption.encrypt(notification)),
queue='email')
return jsonify({'notification_id': notification_id}), 201
@notifications.route('/sms/service/<service_id>', methods=['POST'])
def create_sms_for_service(service_id):
resp_json = request.get_json()
notification, errors = sms_template_notification_schema.load(resp_json)

View File

@@ -7,6 +7,8 @@ from marshmallow import (post_load, ValidationError, validates, validates_schema
mobile_regex = re.compile("^\\+44[\\d]{10}$")
email_regex = re.compile("(^[^@^\\s]+@[^@^\\.^\\s]+(\\.[^@^\\.^\\s]*)*\.(.+))")
# TODO I think marshmallow provides a better integration and error handling.
# Would be better to replace functionality in dao with the marshmallow supported
@@ -78,9 +80,6 @@ class RequestVerifyCodeSchema(ma.Schema):
to = fields.Str(required=False)
# TODO main purpose to be added later
# when processing templates, template will be
# common for all notifications.
class NotificationSchema(ma.Schema):
pass
@@ -94,50 +93,25 @@ class SmsNotificationSchema(NotificationSchema):
raise ValidationError('Invalid phone number, must be of format +441234123123')
class EmailNotificationSchema(NotificationSchema):
to = fields.Str(required=True)
template = fields.Int(required=True)
@validates('to')
def validate_to(self, value):
if not email_regex.match(value):
raise ValidationError('Invalid email')
class SmsTemplateNotificationSchema(SmsNotificationSchema):
template = fields.Int(required=True)
job = fields.String()
@validates_schema
def validate_schema(self, data):
"""
Validate the to field is valid for this notification
"""
from app import api_user
template_id = data.get('template', None)
template = models.Template.query.filter_by(id=template_id).first()
if template:
service = template.service
# Validate restricted service,
# restricted services can only send to one of its users.
if service.restricted:
valid = False
for usr in service.users:
if data['to'] == usr.mobile_number:
valid = True
break
if not valid:
raise ValidationError('Invalid phone number for restricted service', 'restricted')
# Assert the template is valid for the service which made the request.
service = api_user['client']
admin_users = [current_app.config.get('ADMIN_CLIENT_USER_NAME'),
current_app.config.get('DELIVERY_CLIENT_USER_NAME')]
if (service not in admin_users and
template.service != models.Service.query.filter_by(id=service).first()):
raise ValidationError('Invalid template', 'restricted')
class SmsAdminNotificationSchema(SmsNotificationSchema):
content = fields.Str(required=True)
class EmailNotificationSchema(NotificationSchema):
to_address = fields.Str(load_from="to", dump_to='to', required=True)
from_address = fields.Str(load_from="from", dump_to='from', required=True)
subject = fields.Str(required=True)
body = fields.Str(load_from="message", dump_to='message', required=True)
class NotificationStatusSchema(BaseSchema):
class Meta:

View File

@@ -1,4 +1,3 @@
import re
from datetime import datetime
from flask import (
@@ -30,6 +29,7 @@ from app.schemas import (
service_schema,
api_keys_schema
)
from app import email_safe
from flask import Blueprint
@@ -76,7 +76,7 @@ def create_service():
data.pop('user_id', None)
if 'name' in data:
data['email_from'] = _email_safe(data.get('name', None))
data['email_from'] = email_safe(data.get('name', None))
valid_service, errors = service_schema.load(request.get_json())
@@ -87,13 +87,6 @@ def create_service():
return jsonify(data=service_schema.dump(valid_service).data), 201
def _email_safe(string):
return "".join([
character.lower() if character.isalnum() or character == "." else ""
for character in re.sub("\s+", ".", string.strip())
])
@service.route('/<service_id>', methods=['POST'])
def update_service(service_id):
fetched_service = dao_fetch_service_by_id(service_id)