for #309: start sns_inbound view
This commit is contained in:
Родитель
efda3ee5ba
Коммит
d90b8458aa
|
@ -9,11 +9,16 @@ DEBUG=True
|
|||
DJANGO_INTERNAL_IPS=127.0.0.1, localhost
|
||||
SENTRY_DSN=""
|
||||
SERVE_ADDON="private_relay.zip"
|
||||
AWS_REGION=""
|
||||
AWS_ACCESS_KEY_ID=""
|
||||
AWS_SECRET_ACCESS_KEY=""
|
||||
AWS_SNS_TOPIC=""
|
||||
AWS_SES_CONFIGSET=""
|
||||
SOCKETLABS_SERVER_ID=0
|
||||
SOCKETLABS_SECRET_KEY="dummy-value"
|
||||
SOCKETLABS_API_KEY="dummy-value"
|
||||
SOCKETLABS_VALIDATION_KEY="dummy-value"
|
||||
RELAY_FROM_ADDRESS="localhost relay <relay@127.0.0.1:8000>"
|
||||
RELAY_FROM_ADDRESS="relay@127.0.0.1:8000"
|
||||
TWILIO_ACCOUNT_SID=
|
||||
TWILIO_AUTH_TOKEN=
|
||||
TWILIO_SERVICE_ID=
|
||||
|
|
|
@ -6,4 +6,5 @@ from . import views
|
|||
urlpatterns = [
|
||||
path('', views.index, name='emails-index'),
|
||||
path('inbound', views.inbound),
|
||||
path('sns-inbound', views.sns_inbound),
|
||||
]
|
||||
|
|
144
emails/views.py
144
emails/views.py
|
@ -1,11 +1,15 @@
|
|||
from base64 import b64decode
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from email import message_from_string, policy
|
||||
from email.utils import parseaddr
|
||||
from hashlib import sha256
|
||||
import json
|
||||
import logging
|
||||
import markus
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from decouple import config
|
||||
from socketlabs.injectionapi import SocketLabsClient
|
||||
from socketlabs.injectionapi.message.basicmessage import BasicMessage
|
||||
|
@ -22,6 +26,10 @@ from .context_processors import relay_from_domain
|
|||
from .models import DeletedAddress, Profile, RelayAddress
|
||||
|
||||
|
||||
SUPPORTED_SNS_TYPES = [
|
||||
'SubscriptionConfirmation',
|
||||
'Notification',
|
||||
]
|
||||
logger = logging.getLogger('events')
|
||||
metrics = markus.get_metrics('fx-private-relay')
|
||||
|
||||
|
@ -116,6 +124,142 @@ def _index_DELETE(request_data, user_profile):
|
|||
return redirect('profile')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def sns_inbound(request):
|
||||
topic_arn = request.headers.get('X-Amz-Sns-Topic-Arn', None)
|
||||
if not topic_arn:
|
||||
logger.error('SNS inbound request without X-Amz-Sns-Topic-Arn')
|
||||
return HttpResponse(
|
||||
'Received SNS request without Topic ARN.', status=400
|
||||
)
|
||||
if topic_arn != settings.AWS_SNS_TOPIC:
|
||||
logger.error(
|
||||
'SNS message for wrong ARN',
|
||||
extra={
|
||||
'configured_arn': settings.AWS_SNS_TOPIC,
|
||||
'received_arn': topic_arn,
|
||||
}
|
||||
)
|
||||
return HttpResponse(
|
||||
'Received SNS message for wrong topic.', status=400
|
||||
)
|
||||
|
||||
message_type = request.headers.get('X-Amz-Sns-Message-Type', None)
|
||||
if not topic_arn:
|
||||
logger.error('SNS inbound request without X-Amz-Sns-Message-Type')
|
||||
return HttpResponse(
|
||||
'Received SNS request without Message Type.', status=400
|
||||
)
|
||||
if message_type not in SUPPORTED_SNS_TYPES:
|
||||
logger.error(
|
||||
'SNS message for unsupported type',
|
||||
extra={
|
||||
'supported_sns_types': SUPPORTED_SNS_TYPES,
|
||||
'message_type': message_type,
|
||||
}
|
||||
)
|
||||
return HttpResponse(
|
||||
'Received SNS message for unsupported Type: %s' % message_type,
|
||||
status=400
|
||||
)
|
||||
|
||||
json_body = json.loads(request.body)
|
||||
# TODO: Verify request comes from SNS
|
||||
return _sns_inbound_logic(topic_arn, message_type, json_body)
|
||||
|
||||
|
||||
def _sns_inbound_logic(topic_arn, message_type, json_body):
|
||||
if message_type == 'SubscriptionConfirmation':
|
||||
logger.info(
|
||||
'SNS SubscriptionConfirmation',
|
||||
extra={'SubscribeURL': json_body['SubscribeURL']}
|
||||
)
|
||||
return HttpResponse('Logged SubscribeURL', status=200)
|
||||
if message_type == 'Notification':
|
||||
logger.info(
|
||||
'SNS Notification',
|
||||
extra={'json_body': json_body},
|
||||
)
|
||||
return _sns_notification(json_body)
|
||||
|
||||
|
||||
def _sns_notification(json_body):
|
||||
message_json = json.loads(json_body['Message'])
|
||||
notification_type = message_json['notificationType']
|
||||
if notification_type != 'Received':
|
||||
logger.error(
|
||||
'SNS notification for unsupported type',
|
||||
extra={'notification_type': notification_type},
|
||||
)
|
||||
return HttpResponse(
|
||||
'Received SNS notification for unsupported Type: %s' %
|
||||
notification_type,
|
||||
status=400
|
||||
)
|
||||
|
||||
return _sns_mail(message_json)
|
||||
|
||||
|
||||
def _sns_mail(message_json):
|
||||
mail = message_json['mail']
|
||||
to_address = parseaddr(mail['commonHeaders']['to'][0])[1]
|
||||
local_portion = to_address.split('@')[0]
|
||||
|
||||
try:
|
||||
relay_address = RelayAddress.objects.get(address=local_portion)
|
||||
if not relay_address.enabled:
|
||||
relay_address.num_blocked += 1
|
||||
relay_address.save(update_fields=['num_blocked'])
|
||||
return HttpResponse("Address does not exist")
|
||||
except RelayAddress.DoesNotExist:
|
||||
# TODO?: if sha256 of the address is in DeletedAddresses,
|
||||
# create a hard bounce receipt rule
|
||||
logger.error('email_relay', extra={'message_json': message_json})
|
||||
return HttpResponse("Address does not exist", status=404)
|
||||
|
||||
logger.info('email_relay', extra={
|
||||
'fxa_uid': (
|
||||
relay_address.user.socialaccount_set.first().uid
|
||||
),
|
||||
'relay_address_id': relay_address.id,
|
||||
'relay_address': sha256(local_portion.encode('utf-8')).hexdigest(),
|
||||
'real_address': sha256(
|
||||
relay_address.user.email.encode('utf-8')
|
||||
).hexdigest(),
|
||||
})
|
||||
|
||||
from_address = parseaddr(mail['commonHeaders']['from'])[1]
|
||||
subject = mail['commonHeaders']['subject']
|
||||
email_message = message_from_string(message_json['content'])
|
||||
for message_payload in email_message.get_payload():
|
||||
if message_payload.get_content_type() == 'text/plain':
|
||||
text_content = b64decode(
|
||||
message_payload.get_payload()
|
||||
).decode('utf-8')
|
||||
if message_payload.get_content_type() == 'text/html':
|
||||
html_content = b64decode(
|
||||
message_payload.get_payload()
|
||||
).decode('utf-8')
|
||||
|
||||
ses_client = boto3.client('ses', region_name=settings.AWS_REGION)
|
||||
try:
|
||||
ses_response = ses_client.send_email(
|
||||
Destination={'ToAddresses': [relay_address.user.email]},
|
||||
Message={
|
||||
'Body': {
|
||||
'Html': {'Charset': 'UTF-8', 'Data': html_content},
|
||||
'Text': {'Charset': 'UTF-8', 'Data': text_content},
|
||||
},
|
||||
'Subject': {'Charset': 'UTF-8', 'Data': subject},
|
||||
},
|
||||
Source=settings.RELAY_FROM_ADDRESS,
|
||||
ConfigurationSetName=settings.AWS_SES_CONFIGSET,
|
||||
)
|
||||
logger.debug('ses_sent_response', extra=ses_response['MessageId'])
|
||||
except ClientError as e:
|
||||
logger.error('ses_client_error', extra=e.response['Error'])
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def inbound(request):
|
||||
if _get_secret_key(request) != settings.SOCKETLABS_SECRET_KEY:
|
||||
|
|
|
@ -95,10 +95,17 @@ if DJANGO_ALLOWED_SUBNET:
|
|||
# Get our backing resource configs to check if we should install the app
|
||||
ADMIN_ENABLED = config('ADMIN_ENABLED', None)
|
||||
|
||||
|
||||
AWS_REGION = config('AWS_REGION', None)
|
||||
AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID', None)
|
||||
AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY', None)
|
||||
AWS_SNS_TOPIC = config('AWS_SNS_TOPIC', None)
|
||||
AWS_SES_CONFIGSET = config('AWS_SES_CONFIGSET', None)
|
||||
SOCKETLABS_SERVER_ID = config('SOCKETLABS_SERVER_ID', 0, cast=int)
|
||||
SOCKETLABS_API_KEY = config('SOCKETLABS_API_KEY', None)
|
||||
SOCKETLABS_SECRET_KEY = config('SOCKETLABS_SECRET_KEY', None)
|
||||
SOCKETLABS_VALIDATION_KEY = config('SOCKETLABS_VALIDATION_KEY', None)
|
||||
|
||||
RELAY_FROM_ADDRESS = config('RELAY_FROM_ADDRESS', None)
|
||||
|
||||
TWILIO_ACCOUNT_SID = config('TWILIO_ACCOUNT_SID', None)
|
||||
|
@ -140,7 +147,7 @@ if ADMIN_ENABLED:
|
|||
'django.contrib.admin',
|
||||
]
|
||||
|
||||
if SOCKETLABS_API_KEY:
|
||||
if AWS_SNS_TOPIC or SOCKETLABS_API_KEY:
|
||||
INSTALLED_APPS += [
|
||||
'emails.apps.EmailsConfig',
|
||||
]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
boto3==1.13.9
|
||||
Django==2.2.10
|
||||
dj-database-url==0.5.0
|
||||
django-allauth==0.39.1
|
||||
|
|
Загрузка…
Ссылка в новой задаче