def get_pdf_for_notification(notification_id): _data = {"notification_id": notification_id} validate(_data, notification_by_id) notification = notifications_dao.get_notification_by_id( notification_id, authenticated_service.id, _raise=True) if notification.notification_type != LETTER_TYPE: raise BadRequestError(message="Notification is not a letter") if notification.status == NOTIFICATION_VIRUS_SCAN_FAILED: raise BadRequestError(message='Document did not pass the virus scan') if notification.status == NOTIFICATION_TECHNICAL_FAILURE: raise BadRequestError( message='PDF not available for letters in status {}'.format( notification.status)) if notification.status == NOTIFICATION_PENDING_VIRUS_CHECK: raise PDFNotReadyError() try: pdf_data, metadata = get_letter_pdf_and_metadata(notification) except Exception: raise PDFNotReadyError() return send_file(filename_or_fp=BytesIO(pdf_data), mimetype='application/pdf')
def process_precompiled_letter_notifications(*, letter_data, api_key, template, reply_to_text): try: status = NOTIFICATION_PENDING_VIRUS_CHECK letter_content = base64.b64decode(letter_data['content']) pages = pdf_page_count(io.BytesIO(letter_content)) except ValueError: raise BadRequestError(message='Cannot decode letter content (invalid base64 encoding)', status_code=400) except PdfReadError: current_app.logger.exception(msg='Invalid PDF received') raise BadRequestError(message='Letter content is not a valid PDF', status_code=400) notification = create_letter_notification(letter_data=letter_data, template=template, api_key=api_key, status=status, reply_to_text=reply_to_text) filename = upload_letter_pdf(notification, letter_content, precompiled=True) pages_per_sheet = 2 notification.billable_units = math.ceil(pages / pages_per_sheet) dao_update_notification(notification) current_app.logger.info('Calling task scan-file for {}'.format(filename)) # call task to add the filename to anti virus queue notify_celery.send_task( name=TaskNames.SCAN_FILE, kwargs={'filename': filename}, queue=QueueNames.ANTIVIRUS, ) return notification
def validate_and_format_recipient(send_to, key_type, service, notification_type, allow_safelisted_recipients=True): if send_to is None: raise BadRequestError(message="Recipient can't be empty") service_can_send_to_recipient(send_to, key_type, service, allow_safelisted_recipients) if notification_type == SMS_TYPE: international_phone_info = get_international_phone_info(send_to) if international_phone_info.international and INTERNATIONAL_SMS_TYPE not in [ p.permission for p in service.permissions ]: raise BadRequestError( message="Cannot send to international mobile numbers") return validate_and_format_phone_number( number=send_to, international=international_phone_info.international) elif notification_type == EMAIL_TYPE: return validate_and_format_email_address(email_address=send_to)
def create_broadcast(): check_service_has_permission( BROADCAST_TYPE, authenticated_service.permissions, ) if request.content_type != 'application/cap+xml': raise BadRequestError( message=f'Content type {request.content_type} not supported', status_code=415, ) cap_xml = request.get_data() if not validate_xml(cap_xml, 'CAP-v1.2.xsd'): raise BadRequestError( message='Request data is not valid CAP XML', status_code=400, ) broadcast_json = cap_xml_to_dict(cap_xml) validate(broadcast_json, post_broadcast_schema) polygons = Polygons( list( chain.from_iterable( (area['polygons'] for area in broadcast_json['areas'])))) broadcast_message = BroadcastMessage( service_id=authenticated_service.id, content=broadcast_json['content'], reference=broadcast_json['reference'], areas={ 'areas': [area['name'] for area in broadcast_json['areas']], 'simple_polygons': polygons.smooth.simplify.as_coordinate_pairs_long_lat, }, status=BroadcastStatusType.PENDING_APPROVAL, api_key_id=api_user.id, stubbed=authenticated_service.restricted # The client may pass in broadcast_json['expires'] but it’s # simpler for now to ignore it and have the rules around expiry # for broadcasts created with the API match those created from # the admin app ) dao_save_object(broadcast_message) current_app.logger.info( f'Broadcast message {broadcast_message.id} created for service ' f'{authenticated_service.id} with reference {broadcast_json["reference"]}' ) return jsonify(broadcast_message.serialize()), 201
def get_valid_json(): try: request_json = request.get_json(force=True) except BadRequest: raise BadRequestError(message="Invalid JSON supplied in POST data", status_code=400) return request_json or {}
def validate_template(template_id, personalisation, service, notification_type, check_char_count=True): try: template = SerialisedTemplate.from_id_and_service_id( template_id, service.id) except NoResultFound: message = 'Template not found' raise BadRequestError(message=message, fields=[{'template': message}]) check_template_is_for_notification_type(notification_type, template.template_type) check_template_is_active(template) template_with_content = create_content_for_notification( template, personalisation) check_notification_content_is_not_empty(template_with_content) # validating the template in post_notifications happens before the file is uploaded for doc download, # which means the length of the message can be exceeded because it's including the file. # The document download feature is only available through the api. if check_char_count: check_is_message_too_long(template_with_content) return template, template_with_content
def process_document_uploads(personalisation_data, service, simulated, template_id): file_keys = [k for k, v in (personalisation_data or {}).items() if isinstance(v, dict) and "file" in v] if not file_keys: return personalisation_data personalisation_data = personalisation_data.copy() check_service_has_permission(UPLOAD_DOCUMENT, authenticated_service.permissions) for key in file_keys: if simulated: personalisation_data[key] = document_download_client.get_upload_url(service.id) + "/test-document" else: try: personalisation_data[key] = document_download_client.upload_document(service.id, personalisation_data[key]) except DocumentDownloadError as e: raise BadRequestError(message=e.message, status_code=e.status_code) if not simulated: save_stats_for_attachments( [v for k, v in personalisation_data.items() if k in file_keys], service.id, template_id, ) return personalisation_data
def process_document_uploads(personalisation_data, service, simulated=False): file_keys = [ k for k, v in (personalisation_data or {}).items() if isinstance(v, dict) and 'file' in v ] if not file_keys: return personalisation_data personalisation_data = personalisation_data.copy() check_service_has_permission(UPLOAD_DOCUMENT, authenticated_service.permissions) for key in file_keys: if simulated: personalisation_data[ key] = document_download_client.get_upload_url( service.id) + '/test-document' else: try: personalisation_data[ key] = document_download_client.upload_document( service.id, personalisation_data[key]['file']) except DocumentDownloadError as e: raise BadRequestError(message=e.message, status_code=e.status_code) return personalisation_data
def process_document_uploads(personalisation_data, service, simulated=False): """ Returns modified personalisation dict and a count of document uploads. If there are no document uploads, returns a count of `None` rather than `0`. """ file_keys = [ k for k, v in (personalisation_data or {}).items() if isinstance(v, dict) and 'file' in v ] if not file_keys: return personalisation_data, None personalisation_data = personalisation_data.copy() check_if_service_can_send_files_by_email( service_contact_link=authenticated_service.contact_link, service_id=authenticated_service.id) for key in file_keys: if simulated: personalisation_data[ key] = document_download_client.get_upload_url( service.id) + '/test-document' else: try: personalisation_data[ key] = document_download_client.upload_document( service.id, personalisation_data[key]['file']) except DocumentDownloadError as e: raise BadRequestError(message=e.message, status_code=e.status_code) return personalisation_data, len(file_keys)
def process_precompiled_letter_notifications(*, letter_data, api_key, template, reply_to_text): try: status = NOTIFICATION_PENDING_VIRUS_CHECK letter_content = base64.b64decode(letter_data['content']) except ValueError: raise BadRequestError( message='Cannot decode letter content (invalid base64 encoding)', status_code=400) notification = create_letter_notification(letter_data=letter_data, template=template, api_key=api_key, status=status, reply_to_text=reply_to_text) filename = upload_letter_pdf(notification, letter_content, precompiled=True) current_app.logger.info('Calling task scan-file for {}'.format(filename)) # call task to add the filename to anti virus queue if current_app.config['ANTIVIRUS_ENABLED']: notify_celery.send_task( name=TaskNames.SCAN_FILE, kwargs={'filename': filename}, queue=QueueNames.ANTIVIRUS, ) else: # stub out antivirus in dev sanitise_letter.apply_async([filename], queue=QueueNames.LETTERS) return notification
def check_if_service_can_send_files_by_email(service_contact_link, service_id): if not service_contact_link: raise BadRequestError( message= f"Send files by email has not been set up - add contact details for your service at " f"{current_app.config['ADMIN_BASE_URL']}/services/{service_id}/service-settings/send-files-by-email" )
def check_template_exists_by_id_and_service(template_id, service): try: return templates_dao.dao_get_template_by_id_and_service_id( template_id=template_id, service_id=service.id) except NoResultFound: message = "Template not found" raise BadRequestError(message=message, fields=[{"template": message}])
def process_letter_notification(*, letter_data, api_key, template, reply_to_text, precompiled=False): if api_key.key_type == KEY_TYPE_TEAM: raise BadRequestError( message='Cannot send letters with a team api key', status_code=403) if not api_key.service.research_mode and api_key.service.restricted and api_key.key_type != KEY_TYPE_TEST: raise BadRequestError( message='Cannot send letters when service is in trial mode', status_code=403) if precompiled: return process_precompiled_letter_notifications( letter_data=letter_data, api_key=api_key, template=template, reply_to_text=reply_to_text) test_key = api_key.key_type == KEY_TYPE_TEST # if we don't want to actually send the letter, then start it off in SENDING so we don't pick it up status = NOTIFICATION_CREATED if not test_key else NOTIFICATION_SENDING queue = QueueNames.CREATE_LETTERS_PDF if not test_key else QueueNames.RESEARCH_MODE notification = create_letter_notification(letter_data=letter_data, template=template, api_key=api_key, status=status, reply_to_text=reply_to_text) create_letters_pdf.apply_async([str(notification.id)], queue=queue) if test_key: if current_app.config['NOTIFY_ENVIRONMENT'] in [ 'preview', 'development' ]: create_fake_letter_response_file.apply_async( (notification.reference, ), queue=queue) else: update_notification_status_by_reference(notification.reference, NOTIFICATION_DELIVERED) return notification
def check_service_letter_contact_id(service_id, letter_contact_id, notification_type): if letter_contact_id: try: return dao_get_letter_contact_by_id(service_id, letter_contact_id).contact_block except NoResultFound: message = 'letter_contact_id {} does not exist in database for service id {}'\ .format(letter_contact_id, service_id) raise BadRequestError(message=message)
def check_service_sms_sender_id(service_id, sms_sender_id, notification_type): if sms_sender_id: try: return dao_get_service_sms_senders_by_id(service_id, sms_sender_id).sms_sender except NoResultFound: message = 'sms_sender_id {} does not exist in database for service id {}'\ .format(sms_sender_id, service_id) raise BadRequestError(message=message)
def check_service_email_reply_to_id(service_id, reply_to_id, notification_type): if reply_to_id: try: return dao_get_reply_to_by_id(service_id, reply_to_id).email_address except NoResultFound: message = 'email_reply_to_id {} does not exist in database for service id {}'\ .format(reply_to_id, service_id) raise BadRequestError(message=message)
def validate_created_by(service, created_by_id): user = get_user_by_id(created_by_id) if service not in user.services: message = 'Can’t create notification - {} is not part of the "{}" service'.format( user.name, service.name ) raise BadRequestError(message=message)
def check_template_is_active(template): if template.archived: raise BadRequestError( fields=[{ "template": "Template has been deleted" }], message="Template has been deleted", )
def service_can_send_to_recipient(send_to, key_type, service, allow_whitelisted_recipients=True): if not service_allowed_to_send_to(send_to, service, key_type, allow_whitelisted_recipients): if key_type == KEY_TYPE_TEAM: message = 'Can’t send to this recipient using a team-only API key' else: message = ( 'Can’t send to this recipient when service is in trial mode ' '– see https://www.notifications.service.gov.uk/trial-mode' ) raise BadRequestError(message=message)
def post_bulk(): try: request_json = request.get_json() except werkzeug.exceptions.BadRequest as e: raise BadRequestError(message=f"Error decoding arguments: {e.description}", status_code=400) max_rows = current_app.config["CSV_MAX_ROWS"] form = validate(request_json, post_bulk_request(max_rows)) if len([source for source in [form.get("rows"), form.get("csv")] if source]) != 1: raise BadRequestError(message="You should specify either rows or csv", status_code=400) template = validate_template_exists(form["template_id"], authenticated_service) check_service_has_permission(template.template_type, authenticated_service.permissions) remaining_messages = authenticated_service.message_limit - fetch_todays_total_message_count(authenticated_service.id) form["validated_sender_id"] = validate_sender_id(template, form.get("reply_to_id")) try: if form.get("rows"): output = StringIO() writer = csv.writer(output) writer.writerows(form["rows"]) file_data = output.getvalue() else: file_data = form["csv"] recipient_csv = RecipientCSV( file_data, template_type=template.template_type, placeholders=template._as_utils_template().placeholders, max_rows=max_rows, safelist=safelisted_members(authenticated_service, api_user.key_type), remaining_messages=remaining_messages, ) except csv.Error as e: raise BadRequestError(message=f"Error converting to CSV: {str(e)}", status_code=400) check_for_csv_errors(recipient_csv, max_rows, remaining_messages) job = create_bulk_job(authenticated_service, api_user, template, form, recipient_csv) return jsonify(data=job_schema.dump(job).data), 201
def service_can_send_to_recipient(send_to, key_type, service, allow_safelisted_recipients=True): if not service_allowed_to_send_to(send_to, service, key_type, allow_safelisted_recipients): # FIXME: hard code it for now until we can get en/fr specific links and text if key_type == KEY_TYPE_TEAM: message = 'Can’t send to this recipient using a team-only API key '\ f'- see {get_document_url("en", "keys.html#team-and-safelist")}' else: message = ( 'Can’t send to this recipient when service is in trial mode ' f'– see {get_document_url("en", "keys.html#live")}' ) raise BadRequestError(message=message)
def check_is_message_too_long(template_with_content): if template_with_content.is_message_too_long(): message = "Your message is too long. " if template_with_content.template_type == SMS_TYPE: message += ( f"Text messages cannot be longer than {SMS_CHAR_COUNT_LIMIT} characters. " f"Your message is {template_with_content.content_count_without_prefix} characters long." ) elif template_with_content.template_type == EMAIL_TYPE: message += ( f"Emails cannot be longer than 2000000 bytes. " f"Your message is {template_with_content.content_size_in_bytes} bytes." ) raise BadRequestError(message=message)
def check_if_service_can_send_to_number(service, number): international_phone_info = get_international_phone_info(number) if service.permissions and isinstance(service.permissions[0], ServicePermission): permissions = [p.permission for p in service.permissions] else: permissions = service.permissions if ( # if number is international and not a crown dependency international_phone_info.international and not international_phone_info.crown_dependency ) and INTERNATIONAL_SMS_TYPE not in permissions: raise BadRequestError(message="Cannot send to international mobile numbers") else: return international_phone_info
def validate_and_format_recipient(send_to, key_type, service, notification_type, allow_guest_list_recipients=True): if send_to is None: raise BadRequestError(message="Recipient can't be empty") service_can_send_to_recipient(send_to, key_type, service, allow_guest_list_recipients) if notification_type == SMS_TYPE: international_phone_info = check_if_service_can_send_to_number(service, send_to) return validate_and_format_phone_number( number=send_to, international=international_phone_info.international ) elif notification_type == EMAIL_TYPE: return validate_and_format_email_address(email_address=send_to)
def get_reply_to_text(notification_type, sender_id, service, template): reply_to = None if sender_id: try: if notification_type == EMAIL_TYPE: message = "Reply to email address not found" reply_to = dao_get_reply_to_by_id(service.id, sender_id).email_address elif notification_type == SMS_TYPE: message = "SMS sender not found" reply_to = dao_get_service_sms_senders_by_id(service.id, sender_id).get_reply_to_text() except NoResultFound: raise BadRequestError(message=message) else: reply_to = template.get_reply_to_text() return reply_to
def check_for_csv_errors(recipient_csv, max_rows, remaining_messages): nb_rows = len(recipient_csv) if recipient_csv.has_errors: if recipient_csv.missing_column_headers: raise BadRequestError( message=f"Missing column headers: {', '.join(sorted(recipient_csv.missing_column_headers))}", status_code=400, ) if recipient_csv.duplicate_recipient_column_headers: raise BadRequestError( message=f"Duplicate column headers: {', '.join(sorted(recipient_csv.duplicate_recipient_column_headers))}", status_code=400, ) if recipient_csv.more_rows_than_can_send: raise BadRequestError( message=f"You only have {remaining_messages} remaining messages before you reach your daily limit. You've tried to send {nb_rows} messages.", status_code=400, ) if recipient_csv.too_many_rows: raise BadRequestError( message=f"Too many rows. Maximum number of rows allowed is {max_rows}", status_code=400, ) if not recipient_csv.allowed_to_send_to: if api_user.key_type == KEY_TYPE_TEAM: explanation = "because you used a team and safelist API key." if authenticated_service.restricted: explanation = ( "because your service is in trial mode. You can only send to members of your team and your safelist." ) raise BadRequestError( message=f"You cannot send to these recipients {explanation}", status_code=400, ) if recipient_csv.rows_with_errors: def row_error(row): content = [] for header in [header for header in recipient_csv.column_headers if row[header].error]: if row[header].recipient_error: content.append(f"`{header}`: invalid recipient") else: content.append(f"`{header}`: {row[header].error}") return f"Row {row.index} - {','.join(content)}" errors = ". ".join([row_error(row) for row in recipient_csv.initial_rows_with_errors]) raise BadRequestError( message=f"Some rows have errors. {errors}.", status_code=400, ) else: raise NotImplementedError("Got errors but code did not handle")
def validate_template(template_id, personalisation, service, notification_type): try: template = templates_dao.dao_get_template_by_id_and_service_id( template_id=template_id, service_id=service.id) except NoResultFound: message = 'Template not found' raise BadRequestError(message=message, fields=[{'template': message}]) check_template_is_for_notification_type(notification_type, template.template_type) check_template_is_active(template) template_with_content = create_content_for_notification( template, personalisation) if template.template_type == SMS_TYPE: check_sms_content_char_count(template_with_content.content_count) return template, template_with_content
def validate_template(template_id, personalisation, service, notification_type): try: template = SerialisedTemplate.from_id_and_service_id(template_id, service.id) except NoResultFound: message = 'Template not found' raise BadRequestError(message=message, fields=[{'template': message}]) check_template_is_for_notification_type(notification_type, template.template_type) check_template_is_active(template) template_with_content = create_content_for_notification(template, personalisation) check_notification_content_is_not_empty(template_with_content) check_content_char_count(template_with_content) return template, template_with_content
def check_service_has_permission(notify_type, permissions): if not service_has_permission(notify_type, permissions): raise BadRequestError( message="Service is not allowed to send {}".format( get_public_notify_type_text(notify_type, plural=True)))
def check_template_is_for_notification_type(notification_type, template_type): if notification_type != template_type: message = "{0} template is not suitable for {1} notification".format( template_type, notification_type) raise BadRequestError(fields=[{'template': message}], message=message)