updates to views.py
This commit is contained in:
parent
0d9aa2f4a7
commit
4a2e9342ea
|
@ -1,46 +1,30 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import boto3
|
|
||||||
import pytz
|
|
||||||
try:
|
|
||||||
from urllib.request import urlopen
|
|
||||||
from urllib.error import URLError
|
|
||||||
except ImportError:
|
|
||||||
from urllib2 import urlopen, URLError
|
|
||||||
import copy
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
import boto3
|
||||||
|
import pytz
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.contrib.auth import get_user_model
|
||||||
from django.views.decorators.http import require_POST
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.views.generic.base import TemplateView
|
|
||||||
from django.utils.encoding import force_bytes, force_str
|
from django.utils.encoding import force_bytes, force_str
|
||||||
|
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from . import signals
|
from . import signals
|
||||||
from . import utils
|
from . import utils
|
||||||
from .models import (
|
from .models import BounceRecord, ComplaintRecord, SendRecord, UnknownRecord, AwsSesUserAddon
|
||||||
BounceRecord,
|
|
||||||
ComplaintRecord,
|
|
||||||
SendRecord,
|
|
||||||
UnknownRecord,
|
|
||||||
AwsSesUserAddon
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = settings.logger
|
logger = settings.logger
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
def superuser_only(view_func):
|
def superuser_only(view_func):
|
||||||
"""
|
"""Decorator to restrict a view to superusers only."""
|
||||||
Limit a view to superuser only.
|
|
||||||
"""
|
|
||||||
def _inner(request, *args, **kwargs):
|
def _inner(request, *args, **kwargs):
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
@ -49,66 +33,73 @@ def superuser_only(view_func):
|
||||||
|
|
||||||
|
|
||||||
def stats_to_list(stats_dict, localize=pytz):
|
def stats_to_list(stats_dict, localize=pytz):
|
||||||
|
"""Parse SES send statistics into an ordered list of 15-minute summaries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stats_dict (dict): Raw SES statistics data.
|
||||||
|
localize (module): Timezone module (default: pytz).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Sorted list of datapoints with localized timestamps.
|
||||||
"""
|
"""
|
||||||
Parse the output of ``SESConnection.get_send_statistics()`` in to an
|
|
||||||
ordered list of 15-minute summaries.
|
|
||||||
"""
|
|
||||||
# Make a copy, so we don't change the original stats_dict.
|
|
||||||
result = copy.deepcopy(stats_dict)
|
|
||||||
datapoints = []
|
datapoints = []
|
||||||
if localize:
|
current_tz = localize.timezone(settings.TIME_ZONE) if localize else None
|
||||||
current_tz = localize.timezone(settings.TIME_ZONE)
|
|
||||||
else:
|
for dp in stats_dict['SendDataPoints']:
|
||||||
current_tz = None
|
|
||||||
for dp in result['SendDataPoints']:
|
|
||||||
if current_tz:
|
if current_tz:
|
||||||
utc_dt = dp['Timestamp']
|
utc_dt = dp['Timestamp']
|
||||||
dp['Timestamp'] = current_tz.normalize(
|
dp['Timestamp'] = current_tz.normalize(utc_dt.astimezone(current_tz))
|
||||||
utc_dt.astimezone(current_tz))
|
|
||||||
datapoints.append(dp)
|
datapoints.append(dp)
|
||||||
|
|
||||||
datapoints.sort(key=lambda x: x['Timestamp'])
|
return sorted(datapoints, key=lambda x: x['Timestamp'])
|
||||||
|
|
||||||
return datapoints
|
|
||||||
|
|
||||||
|
|
||||||
def emails_parse(emails_dict):
|
def emails_parse(emails_dict):
|
||||||
|
"""Parse SES verified email addresses into a sorted list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emails_dict (dict): Raw SES verified email data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Sorted list of verified email addresses.
|
||||||
"""
|
"""
|
||||||
Parse the output of ``SESConnection.list_verified_emails()`` and get
|
return sorted(emails_dict['VerifiedEmailAddresses'])
|
||||||
a list of emails.
|
|
||||||
"""
|
|
||||||
return sorted([email for email in emails_dict['VerifiedEmailAddresses']])
|
|
||||||
|
|
||||||
|
|
||||||
def sum_stats(stats_data):
|
def sum_stats(stats_data):
|
||||||
"""
|
"""Summarize SES statistics from a list of datapoints.
|
||||||
Summarize the bounces, complaints, delivery attempts and rejects from a
|
|
||||||
list of datapoints.
|
|
||||||
"""
|
|
||||||
t_bounces = 0
|
|
||||||
t_complaints = 0
|
|
||||||
t_delivery_attempts = 0
|
|
||||||
t_rejects = 0
|
|
||||||
for dp in stats_data:
|
|
||||||
t_bounces += dp['Bounces']
|
|
||||||
t_complaints += dp['Complaints']
|
|
||||||
t_delivery_attempts += dp['DeliveryAttempts']
|
|
||||||
t_rejects += dp['Rejects']
|
|
||||||
|
|
||||||
return {
|
Args:
|
||||||
'Bounces': t_bounces,
|
stats_data (list): List of SES datapoints.
|
||||||
'Complaints': t_complaints,
|
|
||||||
'DeliveryAttempts': t_delivery_attempts,
|
Returns:
|
||||||
'Rejects': t_rejects,
|
dict: Summary of bounces, complaints, delivery attempts, and rejects.
|
||||||
|
"""
|
||||||
|
summary = {
|
||||||
|
'Bounces': 0,
|
||||||
|
'Complaints': 0,
|
||||||
|
'DeliveryAttempts': 0,
|
||||||
|
'Rejects': 0,
|
||||||
}
|
}
|
||||||
|
for dp in stats_data:
|
||||||
|
summary['Bounces'] += dp['Bounces']
|
||||||
|
summary['Complaints'] += dp['Complaints']
|
||||||
|
summary['DeliveryAttempts'] += dp['DeliveryAttempts']
|
||||||
|
summary['Rejects'] += dp['Rejects']
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
@superuser_only
|
@superuser_only
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
|
"""Display SES send statistics dashboard for superusers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: HTTP request object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: Rendered dashboard with SES statistics.
|
||||||
"""
|
"""
|
||||||
Graph SES send statistics over time.
|
cache_key = 'django_aws_ses_status'
|
||||||
"""
|
|
||||||
cache_key = 'vhash:django_aws_ses_status'
|
|
||||||
cached_view = cache.get(cache_key)
|
cached_view = cache.get(cache_key)
|
||||||
if cached_view:
|
if cached_view:
|
||||||
return cached_view
|
return cached_view
|
||||||
|
@ -118,337 +109,220 @@ def dashboard(request):
|
||||||
aws_access_key_id=settings.ACCESS_KEY,
|
aws_access_key_id=settings.ACCESS_KEY,
|
||||||
aws_secret_access_key=settings.SECRET_KEY,
|
aws_secret_access_key=settings.SECRET_KEY,
|
||||||
region_name=settings.AWS_SES_REGION_NAME,
|
region_name=settings.AWS_SES_REGION_NAME,
|
||||||
endpoint_url=settings.AWS_SES_REGION_ENDPOINT_URL,
|
endpoint_url=settings.AWS_SES_REGION_ENDPOINT,
|
||||||
)
|
)
|
||||||
|
|
||||||
quota_dict = ses_conn.get_send_quota()
|
try:
|
||||||
verified_emails_dict = ses_conn.list_verified_email_addresses()
|
quota_dict = ses_conn.get_send_quota()
|
||||||
stats = ses_conn.get_send_statistics()
|
verified_emails_dict = ses_conn.list_verified_email_addresses()
|
||||||
|
stats = ses_conn.get_send_statistics()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to fetch SES statistics: {e}")
|
||||||
|
return HttpResponseBadRequest("Failed to fetch SES statistics")
|
||||||
|
|
||||||
verified_emails = emails_parse(verified_emails_dict)
|
verified_emails = emails_parse(verified_emails_dict)
|
||||||
ordered_data = stats_to_list(stats)
|
ordered_data = stats_to_list(stats)
|
||||||
summary = sum_stats(ordered_data)
|
summary = sum_stats(ordered_data)
|
||||||
|
|
||||||
extra_context = {
|
context = {
|
||||||
'title': 'SES Statistics',
|
'title': 'SES Statistics',
|
||||||
'datapoints': ordered_data,
|
'datapoints': ordered_data,
|
||||||
'24hour_quota': quota_dict['Max24HourSend'],
|
'24hour_quota': quota_dict['Max24HourSend'],
|
||||||
'24hour_sent': quota_dict['SentLast24Hours'],
|
'24hour_sent': quota_dict['SentLast24Hours'],
|
||||||
'24hour_remaining':
|
'24hour_remaining': quota_dict['Max24HourSend'] - quota_dict['SentLast24Hours'],
|
||||||
quota_dict['Max24HourSend'] -
|
|
||||||
quota_dict['SentLast24Hours'],
|
|
||||||
'persecond_rate': quota_dict['MaxSendRate'],
|
'persecond_rate': quota_dict['MaxSendRate'],
|
||||||
'verified_emails': verified_emails,
|
'verified_emails': verified_emails,
|
||||||
'summary': summary,
|
'summary': summary,
|
||||||
'access_key': settings.ACCESS_KEY,
|
|
||||||
'local_time': True,
|
'local_time': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = render(request, 'django_aws_ses/send_stats.html', extra_context)
|
response = render(request, 'django_aws_ses/send_stats.html', context)
|
||||||
|
|
||||||
cache.set(cache_key, response, 60 * 15) # Cache for 15 minutes
|
cache.set(cache_key, response, 60 * 15) # Cache for 15 minutes
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def handle_bounce(request):
|
def handle_bounce(request):
|
||||||
|
"""Handle AWS SES/SNS bounce, complaint, or delivery notifications.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: HTTP request object with SNS notification JSON.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: HTTP 200 for successful processing, 400 for invalid JSON.
|
||||||
"""
|
"""
|
||||||
Handle a bounced email via an SNS webhook.
|
logger.info("Received SNS callback")
|
||||||
|
|
||||||
Parse the bounced message and send the appropriate signal.
|
|
||||||
For bounce messages the bounce_received signal is called.
|
|
||||||
For complaint messages the complaint_received signal is called.
|
|
||||||
See: http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html#http-subscription-confirmation-json
|
|
||||||
See: http://docs.amazonwebservices.com/ses/latest/DeveloperGuide/NotificationsViaSNS.html
|
|
||||||
|
|
||||||
In addition to email bounce requests this endpoint also supports the SNS
|
|
||||||
subscription confirmation request. This request is sent to the SNS
|
|
||||||
subscription endpoint when the subscription is registered.
|
|
||||||
See: http://docs.aws.amazon.com/sns/latest/gsg/Subscribe.html
|
|
||||||
|
|
||||||
For the format of the SNS subscription confirmation request see this URL:
|
|
||||||
http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html#http-subscription-confirmation-json
|
|
||||||
|
|
||||||
SNS message signatures are verified by default. This functionality can
|
|
||||||
be disabled by setting AWS_SES_VERIFY_BOUNCE_SIGNATURES to False.
|
|
||||||
However, this is not recommended.
|
|
||||||
See: http://docs.amazonwebservices.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
|
|
||||||
"""
|
|
||||||
logger.warning(u'Received SNS call back')
|
|
||||||
|
|
||||||
|
|
||||||
raw_json = request.body
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
notification = json.loads(raw_json.decode('utf-8'))
|
notification = json.loads(request.body.decode('utf-8'))
|
||||||
except ValueError as e:
|
except (ValueError, UnicodeDecodeError) as e:
|
||||||
# TODO: What kind of response should be returned here?
|
logger.warning(f"Invalid SNS notification JSON: {e}")
|
||||||
logger.warning(u'Received bounce with bad JSON: "%s"', e)
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
# Verify the authenticity of the bounce message.
|
if settings.VERIFY_BOUNCE_SIGNATURES and not utils.verify_bounce_message(notification):
|
||||||
if (settings.VERIFY_BOUNCE_SIGNATURES and
|
logger.warning(f"Unverified SNS notification: Type={notification.get('Type')}")
|
||||||
not utils.verify_bounce_message(notification)):
|
|
||||||
# Don't send any info back when the notification is not
|
|
||||||
# verified. Simply, don't process it.
|
|
||||||
logger.info(
|
|
||||||
u'Received unverified notification: Type: %s',
|
|
||||||
notification.get('Type'),
|
|
||||||
extra={
|
|
||||||
'notification': notification,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
logger.info('notification.get("Type"): %s' % notification.get("Type"))
|
|
||||||
if notification.get('Type') in ('SubscriptionConfirmation',
|
|
||||||
'UnsubscribeConfirmation'):
|
|
||||||
# Process the (un)subscription confirmation.
|
|
||||||
|
|
||||||
logger.info(
|
notification_type = notification.get('Type')
|
||||||
u'Received subscription confirmation: TopicArn: %s',
|
if notification_type in ('SubscriptionConfirmation', 'UnsubscribeConfirmation'):
|
||||||
notification.get('TopicArn'),
|
logger.info(f"Received {notification_type}: TopicArn={notification.get('TopicArn')}")
|
||||||
extra={
|
|
||||||
'notification': notification,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the subscribe url and hit the url to confirm the subscription.
|
|
||||||
subscribe_url = notification.get('SubscribeURL')
|
subscribe_url = notification.get('SubscribeURL')
|
||||||
try:
|
if subscribe_url:
|
||||||
urlopen(subscribe_url).read()
|
try:
|
||||||
except URLError as e:
|
import requests
|
||||||
# Some kind of error occurred when confirming the request.
|
response = requests.get(subscribe_url)
|
||||||
logger.error(
|
response.raise_for_status()
|
||||||
u'Could not confirm subscription: "%s"', e,
|
except requests.RequestException as e:
|
||||||
extra={
|
logger.error(f"Failed to confirm {notification_type}: {e}")
|
||||||
'notification': notification,
|
return HttpResponse()
|
||||||
},
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
elif notification.get('Type') == 'Notification':
|
|
||||||
try:
|
|
||||||
message = json.loads(notification['Message'])
|
|
||||||
except ValueError as e:
|
|
||||||
# The message isn't JSON.
|
|
||||||
# Just ignore the notification.
|
|
||||||
logger.warning(u'Received bounce with bad JSON: "%s"', e, extra={
|
|
||||||
'notification': notification,
|
|
||||||
})
|
|
||||||
|
|
||||||
else:
|
if notification_type != 'Notification':
|
||||||
|
UnknownRecord.objects.create(event_type=notification_type, aws_data=str(notification))
|
||||||
|
logger.info(f"Received unknown notification type: {notification_type}")
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
mail_obj = message.get('mail')
|
try:
|
||||||
event_type = message.get('notificationType', message.get('eventType'))
|
message = json.loads(notification['Message'])
|
||||||
logger.info('event_type: %s' % event_type)
|
except ValueError as e:
|
||||||
if event_type == 'Bounce':
|
logger.warning(f"Invalid message JSON in notification: {e}")
|
||||||
# Bounce
|
return HttpResponse()
|
||||||
bounce_obj = message.get('bounce', {})
|
|
||||||
|
|
||||||
# Logging
|
mail_obj = message.get('mail', {})
|
||||||
feedback_id = bounce_obj.get('feedbackId')
|
event_type = message.get('notificationType', message.get('eventType', 'Unknown'))
|
||||||
bounce_type = bounce_obj.get('bounceType')
|
|
||||||
bounce_subtype = bounce_obj.get('bounceSubType')
|
|
||||||
bounce_recipients = bounce_obj.get('bouncedRecipients', [])
|
|
||||||
logger.info(
|
|
||||||
u'Received bounce notification: feedbackId: %s, bounceType: %s, bounceSubType: %s',
|
|
||||||
feedback_id, bounce_type, bounce_subtype,
|
|
||||||
extra={
|
|
||||||
'notification': notification,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# create a BounceRecord so we can keep from sending to bad emails.
|
if event_type == 'Bounce':
|
||||||
logger.info('create records')
|
bounce_obj = message.get('bounce', {})
|
||||||
for recipient in bounce_recipients:
|
feedback_id = bounce_obj.get('feedbackId')
|
||||||
logger.info('recipient: %s' % recipient)
|
bounce_type = bounce_obj.get('bounceType')
|
||||||
BounceRecord.objects.create(
|
bounce_subtype = bounce_obj.get('bounceSubType')
|
||||||
email = recipient.get('emailAddress', None),
|
logger.info(f"Received bounce: feedbackId={feedback_id}, type={bounce_type}, subtype={bounce_subtype}")
|
||||||
status = recipient.get('status', None),
|
|
||||||
action = recipient.get('action', None),
|
|
||||||
diagnostic_code = recipient.get('diagnosticCode', None),
|
|
||||||
bounce_type = bounce_obj.get('bounceType', None),
|
|
||||||
bounce_sub_type = bounce_obj.get('bounceSubType', None),
|
|
||||||
feedback_id = bounce_obj.get('feedbackId', None),
|
|
||||||
reporting_mta = bounce_obj.get('reportingMTA', None),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
for recipient in bounce_obj.get('bouncedRecipients', []):
|
||||||
signals.bounce_received.send(
|
BounceRecord.objects.create(
|
||||||
sender=handle_bounce,
|
email=recipient.get('emailAddress'),
|
||||||
mail_obj=mail_obj,
|
status=recipient.get('status'),
|
||||||
bounce_obj=bounce_obj,
|
action=recipient.get('action'),
|
||||||
raw_message=raw_json,
|
diagnostic_code=recipient.get('diagnosticCode'),
|
||||||
)
|
bounce_type=bounce_type,
|
||||||
|
bounce_sub_type=bounce_subtype,
|
||||||
elif event_type == 'Complaint':
|
feedback_id=feedback_id,
|
||||||
# Complaint
|
reporting_mta=bounce_obj.get('reportingMTA'),
|
||||||
complaint_obj = message.get('complaint', {})
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
feedback_id = complaint_obj.get('feedbackId')
|
|
||||||
feedback_type = complaint_obj.get('complaintFeedbackType')
|
|
||||||
complaint_recipients = complaint_obj.get('complainedRecipients')
|
|
||||||
logger.info('create records')
|
|
||||||
for recipient in complaint_recipients:
|
|
||||||
logger.info('recipient: %s' % recipient)
|
|
||||||
ComplaintRecord.objects.create(
|
|
||||||
email = recipient.get('emailAddress', None),
|
|
||||||
sub_type = complaint_obj.get('complaintSubType', None),
|
|
||||||
feedback_id = complaint_obj.get('feedbackId', None),
|
|
||||||
feedback_type = complaint_obj.get('complaintFeedbackType', None),
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
u'Received complaint notification: feedbackId: %s, feedbackType: %s',
|
|
||||||
feedback_id, feedback_type,
|
|
||||||
extra={
|
|
||||||
'notification': notification,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
signals.complaint_received.send(
|
|
||||||
sender=handle_bounce,
|
|
||||||
mail_obj=mail_obj,
|
|
||||||
complaint_obj=complaint_obj,
|
|
||||||
raw_message=raw_json,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif event_type in ['Delivery','Send']:
|
|
||||||
# Delivery
|
|
||||||
send_obj = message.get('mail', {})
|
|
||||||
|
|
||||||
logger.info('send_obj: %s' % send_obj)
|
|
||||||
|
|
||||||
source = send_obj.get('source', 'N/A')#settings.DEFAULT_FROM_EMAIL)
|
|
||||||
destinations = send_obj.get('destination', [])
|
|
||||||
message_id = send_obj.get('messageId','N/A')
|
|
||||||
delivery = message.get('delivery', None)
|
|
||||||
aws_process_time = -1
|
|
||||||
smtp_response = 'N/A'
|
|
||||||
if delivery:
|
|
||||||
logger.info('we are a delivery and had a delivery key')
|
|
||||||
aws_process_time = delivery.get('processingTimeMillis',0)
|
|
||||||
smtp_response = delivery.get('smtpResponse', 'N/A')
|
|
||||||
|
|
||||||
common_headers = send_obj.get('commonHeaders', None)
|
|
||||||
subject = "N/A"
|
|
||||||
if common_headers:
|
|
||||||
subject = common_headers.get('subject','N/A')
|
|
||||||
status = event_type
|
|
||||||
logger.info('create records')
|
|
||||||
logger.info('destinations: %s' % destinations)
|
|
||||||
for destination in destinations:
|
|
||||||
try:
|
|
||||||
logger.info('destination: %s' % destination)
|
|
||||||
send_record, created = SendRecord.objects.get_or_create(
|
|
||||||
source = source,
|
|
||||||
destination = destination,
|
|
||||||
status = status,
|
|
||||||
message_id = message_id,
|
|
||||||
defaults={
|
|
||||||
"aws_process_time": aws_process_time,
|
|
||||||
"smtp_response": smtp_response,
|
|
||||||
"subject": subject
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
if send_record.subject == "N/A":
|
|
||||||
send_record.subject = subject
|
|
||||||
|
|
||||||
if send_record.smtp_response == "N/A":
|
|
||||||
send_record.smtp_response = smtp_response
|
|
||||||
|
|
||||||
if send_record.aws_process_time == -1:
|
|
||||||
send_record.aws_process_time = aws_process_time
|
|
||||||
|
|
||||||
send_record.save()
|
|
||||||
except Exception as e:
|
|
||||||
logger.info("error well trying to get_or_create record: %s" % e)
|
|
||||||
logger.info(
|
|
||||||
u'Received delivery notification: messageId: %s',
|
|
||||||
message_id,
|
|
||||||
extra={
|
|
||||||
'notification': notification,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
signals.delivery_received.send(
|
|
||||||
sender=handle_bounce,
|
|
||||||
mail_obj=mail_obj,
|
|
||||||
delivery_obj=send_obj,
|
|
||||||
raw_message=raw_json,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# We received an unknown notification type. Just log and
|
|
||||||
# ignore it.
|
|
||||||
|
|
||||||
UnknownRecord.objects.create(
|
|
||||||
event_type = eventType,
|
|
||||||
aws_data = str(notification)
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.warning(u"Received unknown event", extra={
|
|
||||||
'notification': notification,
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
|
|
||||||
UnknownRecord.objects.create(
|
|
||||||
eventType = notification.get('Type'),
|
|
||||||
aws_data = str(notification)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
signals.bounce_received.send(
|
||||||
u'Received unknown notification type: %s',
|
sender=handle_bounce,
|
||||||
notification.get('Type'),
|
mail_obj=mail_obj,
|
||||||
extra={
|
bounce_obj=bounce_obj,
|
||||||
'notification': notification,
|
raw_message=request.body,
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# AWS will consider anything other than 200 to be an error response and
|
elif event_type == 'Complaint':
|
||||||
# resend the SNS request. We don't need that so we return 200 here.
|
complaint_obj = message.get('complaint', {})
|
||||||
|
feedback_id = complaint_obj.get('feedbackId')
|
||||||
|
feedback_type = complaint_obj.get('complaintFeedbackType')
|
||||||
|
logger.info(f"Received complaint: feedbackId={feedback_id}, type={feedback_type}")
|
||||||
|
|
||||||
|
for recipient in complaint_obj.get('complainedRecipients', []):
|
||||||
|
ComplaintRecord.objects.create(
|
||||||
|
email=recipient.get('emailAddress'),
|
||||||
|
sub_type=complaint_obj.get('complaintSubType'),
|
||||||
|
feedback_id=feedback_id,
|
||||||
|
feedback_type=feedback_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
signals.complaint_received.send(
|
||||||
|
sender=handle_bounce,
|
||||||
|
mail_obj=mail_obj,
|
||||||
|
complaint_obj=complaint_obj,
|
||||||
|
raw_message=request.body,
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event_type in ('Delivery', 'Send'):
|
||||||
|
send_obj = mail_obj
|
||||||
|
source = send_obj.get('source', settings.DEFAULT_FROM_EMAIL)
|
||||||
|
destinations = send_obj.get('destination', [])
|
||||||
|
message_id = send_obj.get('messageId', 'N/A')
|
||||||
|
delivery = message.get('delivery', {})
|
||||||
|
aws_process_time = delivery.get('processingTimeMillis', 0)
|
||||||
|
smtp_response = delivery.get('smtpResponse', 'N/A')
|
||||||
|
subject = send_obj.get('commonHeaders', {}).get('subject', 'N/A')
|
||||||
|
|
||||||
|
logger.info(f"Received {event_type} notification: messageId={message_id}")
|
||||||
|
|
||||||
|
for destination in destinations:
|
||||||
|
try:
|
||||||
|
send_record, created = SendRecord.objects.get_or_create(
|
||||||
|
source=source,
|
||||||
|
destination=destination,
|
||||||
|
status=event_type,
|
||||||
|
message_id=message_id,
|
||||||
|
defaults={
|
||||||
|
"aws_process_time": aws_process_time,
|
||||||
|
"smtp_response": smtp_response,
|
||||||
|
"subject": subject,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if send_record.subject == "N/A":
|
||||||
|
send_record.subject = subject
|
||||||
|
if send_record.smtp_response == "N/A":
|
||||||
|
send_record.smtp_response = smtp_response
|
||||||
|
if send_record.aws_process_time == 0:
|
||||||
|
send_record.aws_process_time = aws_process_time
|
||||||
|
send_record.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save SendRecord for {destination}: {e}")
|
||||||
|
|
||||||
|
signals.delivery_received.send(
|
||||||
|
sender=handle_bounce,
|
||||||
|
mail_obj=mail_obj,
|
||||||
|
delivery_obj=send_obj,
|
||||||
|
raw_message=request.body,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
UnknownRecord.objects.create(event_type=event_type, aws_data=str(notification))
|
||||||
|
logger.warning(f"Received unknown event: {event_type}")
|
||||||
|
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
class HandleUnsubscribe(TemplateView):
|
class HandleUnsubscribe(TemplateView):
|
||||||
|
"""View to handle email unsubscribe requests."""
|
||||||
http_method_names = ['get']
|
http_method_names = ['get']
|
||||||
|
template_name = settings.UNSUBSCRIBE_TEMPLATE
|
||||||
template_name = settings.UNSUBSCRIBE_TEMPLET
|
base_template_name = settings.BASE_TEMPLATE
|
||||||
base_template_name = settings.BASE_TEMPLET
|
|
||||||
unsubscribe_message = "We Have Unsubscribed the Following Email"
|
unsubscribe_message = "We Have Unsubscribed the Following Email"
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
"""Add base template and unsubscribe message to context."""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
logger.info("in get_context_data ----- self.base_template_name: %s" % self.base_template_name)
|
|
||||||
context['base_template_name'] = self.base_template_name
|
context['base_template_name'] = self.base_template_name
|
||||||
context['unsubscribe_message'] = self.unsubscribe_message
|
context['unsubscribe_message'] = self.unsubscribe_message
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""Process unsubscribe request and redirect if invalid."""
|
||||||
uuid = self.kwargs['uuid']
|
uuid = self.kwargs['uuid']
|
||||||
hash = self.kwargs['hash']
|
hash_value = self.kwargs['hash']
|
||||||
|
|
||||||
logger.info("in get ----- self.base_template_name: %s" % self.base_template_name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
uuid = force_str(urlsafe_base64_decode(uuid).decode())
|
uuid = force_str(urlsafe_base64_decode(uuid))
|
||||||
logger.info('uuid: %s' % uuid)
|
|
||||||
user = User.objects.get(pk=uuid)
|
user = User.objects.get(pk=uuid)
|
||||||
logger.info('user.pk: %s' % user.pk)
|
except (TypeError, ValueError, OverflowError, User.DoesNotExist) as e:
|
||||||
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
logger.warning(f"Invalid unsubscribe UUID: {e}")
|
||||||
return redirect(settings.HOME_URL)
|
return redirect(settings.HOME_URL)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ses = user.aws_ses
|
ses = user.aws_ses
|
||||||
except AwsSesUserAddon.DoesNotExist:
|
except AwsSesUserAddon.DoesNotExist:
|
||||||
ses = AwsSesUserAddon.objects.create(user=user)
|
ses = AwsSesUserAddon.objects.create(user=user)
|
||||||
|
|
||||||
if user is not None and user.aws_ses.check_unsubscribe_hash(hash):
|
if user and ses.check_unsubscribe_hash(hash_value):
|
||||||
logger.info('ses.pk: %s' % ses.pk)
|
|
||||||
ses.unsubscribe = True
|
ses.unsubscribe = True
|
||||||
ses.save()
|
ses.save()
|
||||||
|
logger.info(f"Unsubscribed user: {user.email}")
|
||||||
else:
|
else:
|
||||||
logger.warning("bad hash was provided!")
|
logger.warning(f"Invalid unsubscribe hash for user: {user.email}")
|
||||||
return redirect(settings.HOME_URL)
|
return redirect(settings.HOME_URL)
|
||||||
|
|
||||||
return super(HandleUnsubscribe, self).get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
Loading…
Reference in New Issue