first commit of python module project

git-svn-id: https://vault.zeeksgeeks.com/svn/django_aws_ses/trunk@4 ed966f06-d3d6-432b-bc91-693151a5c6b4
This commit is contained in:
Raymond Jessop 2021-02-19 01:59:04 +00:00
parent 014debbf9d
commit 6329154ffa
17 changed files with 1533 additions and 0 deletions

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>trunk</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

5
.pydevproject Normal file
View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
</pydev_project>

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2011 Harry Marr
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

0
README.md Normal file
View File

View File

@ -0,0 +1,8 @@
default_app_config = 'django_aws_ses.apps.DjangoAwsSesBackendConfig'
# When changing this, remember to change it in setup.py
VERSION = (0, 0, 1)
__version__ = '.'.join([str(x) for x in VERSION])
__author__ = 'Ray Jessop'
__all__ = ('Django AWS SES Backend',)

76
django_aws_ses/admin.py Normal file
View File

@ -0,0 +1,76 @@
from django.contrib import admin
from .models import (
AwsSesSettings,
SESStat,
BounceRecord,
AwsSesUserAddon,
ComplaintRecord,
SendRecord,
UnknownRecord
)
from . import settings
logger = settings.logger
class AwsSesSettingsAdmin(admin.ModelAdmin):
model = AwsSesSettings
list_display = ('get_site', 'region_name')
def get_site(self, obj):
return obj.site.domain
get_site.short_description = 'domain'
get_site.admin_order_field = 'site__domain'
admin.site.register(AwsSesSettings, AwsSesSettingsAdmin)
class AwsSesUserAddonAdmin(admin.ModelAdmin):
model = AwsSesUserAddon
list_display = ('get_email', 'unsubscribe')
def get_email(self, obj):
return obj.user.email
get_email.short_description = 'email'
get_email.admin_order_field = 'user__email'
admin.site.register(AwsSesUserAddon, AwsSesUserAddonAdmin)
class SESStatAdmin(admin.ModelAdmin):
model = SESStat
list_display = ('date', 'delivery_attempts', 'bounces', 'complaints', 'rejects')
admin.site.register(SESStat, SESStatAdmin)
class AdminEmailListFilter(admin.SimpleListFilter):
def queryset(self, request, queryset):
logger.info('self.value(): %s' % self.value())
return queryset.filter(email__contains=self.value())
class BounceRecordAdmin(admin.ModelAdmin):
model = BounceRecord
list_display = ('email', 'bounce_type', 'bounce_sub_type', 'status', 'timestamp')
list_filter = ('email', 'bounce_type', 'bounce_sub_type', 'status', 'timestamp')
admin.site.register(BounceRecord, BounceRecordAdmin)
class ComplaintRecordAdmin(admin.ModelAdmin):
model = ComplaintRecord
list_display = ('email', 'sub_type', 'feedback_type', 'timestamp')
list_filter = ('email', 'sub_type', 'feedback_type', 'timestamp')
admin.site.register(ComplaintRecord, ComplaintRecordAdmin)
class SendRecordAdmin(admin.ModelAdmin):
model = ComplaintRecord
list_display = ('source', 'destination', 'subject', 'timestamp', 'status')
list_filter = ('source', 'destination', 'subject', 'timestamp', 'status')
admin.site.register(SendRecord, SendRecordAdmin)
class UnknownRecordAdmin(admin.ModelAdmin):
model = ComplaintRecord
list_display = ('event_type', 'aws_data')
list_filter = ('event_type', 'aws_data')
admin.site.register(UnknownRecord, UnknownRecordAdmin)

6
django_aws_ses/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DjangoAwsSesBackendConfig(AppConfig):
name = 'django_aws_ses'
verbose_name = 'Django AWS SES'

253
django_aws_ses/backends.py Normal file
View File

@ -0,0 +1,253 @@
import logging
import boto3
from botocore.vendored.requests.packages.urllib3.exceptions import ResponseError
from django.core.mail.backends.base import BaseEmailBackend
from django.db.models import Count
from django.dispatch import Signal
from datetime import datetime, timedelta
from time import sleep
from . import settings
from . import signals
from . import utils
from .models import BounceRecord
logger = settings.logger
cached_rate_limits = {}
recent_send_times = []
def dkim_sign(message, dkim_domain=None, dkim_key=None, dkim_selector=None, dkim_headers=None):
"""Return signed email message if dkim package and settings are available."""
try:
import dkim
except ImportError:
pass
else:
if dkim_domain and dkim_key:
sig = dkim.sign(message,
dkim_selector,
dkim_domain,
dkim_key,
include_headers=dkim_headers)
message = sig + message
return message
class SESBackend(BaseEmailBackend):
"""A Django Email backend that uses Amazon's Simple Email Service.
"""
def __init__(self, fail_silently=False, aws_access_key=None,
aws_secret_key=None, aws_region_name=None,
aws_region_endpoint=None, aws_auto_throttle=None,
dkim_domain=None, dkim_key=None, dkim_selector=None,
dkim_headers=None, **kwargs):
super(SESBackend, self).__init__(fail_silently=fail_silently, **kwargs)
self._access_key_id = aws_access_key or settings.ACCESS_KEY
self._access_key = aws_secret_key or settings.SECRET_KEY
self._region_name = aws_region_name if aws_region_name else settings.AWS_SES_REGION_NAME
self._endpoint_url = aws_region_endpoint if aws_region_endpoint else settings.AWS_SES_REGION_ENDPOINT_URL
self._throttle = aws_auto_throttle or settings.AWS_SES_AUTO_THROTTLE
self.dkim_domain = dkim_domain or settings.DKIM_DOMAIN
self.dkim_key = dkim_key or settings.DKIM_PRIVATE_KEY
self.dkim_selector = dkim_selector or settings.DKIM_SELECTOR
self.dkim_headers = dkim_headers or settings.DKIM_HEADERS
self.connection = None
def open(self):
"""Create a connection to the AWS API server. This can be reused for
sending multiple emails.
"""
if self.connection:
return False
try:
self.connection = boto3.client(
'ses',
aws_access_key_id=self._access_key_id,
aws_secret_access_key=self._access_key,
region_name=self._region_name,
endpoint_url=self._endpoint_url,
)
except Exception:
if not self.fail_silently:
raise
def close(self):
"""Close any open HTTP connections to the API server.
"""
self.connection = None
def send_messages(self, email_messages):
"""Sends one or more EmailMessage objects and returns the number of
email messages sent.
"""
logger.info("send_messages")
if not email_messages:
return
new_conn_created = self.open()
if not self.connection:
# Failed silently
return
num_sent = 0
source = settings.AWS_SES_RETURN_PATH
logger.info("email_messages: %s" % email_messages)
for message in email_messages:
# SES Configuration sets. If the AWS_SES_CONFIGURATION_SET setting
# is not None, append the appropriate header to the message so that
# SES knows which configuration set it belongs to.
#
# If settings.AWS_SES_CONFIGURATION_SET is a callable, pass it the
# message object and dkim settings and expect it to return a string
# containing the SES Configuration Set name.
logger.info("Sending signal(email_pre_send)")
signals.email_pre_send.send_robust(self.__class__, message=message)
message.to = utils.filter_recipiants(message.recipients())
logger.info("message.recipients() after email_pre_send: %s" % message.recipients())
if not message.recipients():
logger.info("no recipients left after the filter")
return False
if (settings.AWS_SES_CONFIGURATION_SET
and 'X-SES-CONFIGURATION-SET' not in message.extra_headers):
if callable(settings.AWS_SES_CONFIGURATION_SET):
message.extra_headers[
'X-SES-CONFIGURATION-SET'] = settings.AWS_SES_CONFIGURATION_SET(
message,
dkim_domain=self.dkim_domain,
dkim_key=self.dkim_key,
dkim_selector=self.dkim_selector,
dkim_headers=self.dkim_headers
)
else:
message.extra_headers[
'X-SES-CONFIGURATION-SET'] = settings.AWS_SES_CONFIGURATION_SET
# Automatic throttling. Assumes that this is the only SES client
# currently operating. The AWS_SES_AUTO_THROTTLE setting is a
# factor to apply to the rate limit, with a default of 0.5 to stay
# well below the actual SES throttle.
# Set the setting to 0 or None to disable throttling.
if self._throttle:
global recent_send_times
now = datetime.now()
# Get and cache the current SES max-per-second rate limit
# returned by the SES API.
rate_limit = self.get_rate_limit()
logger.debug(u"send_messages.throttle rate_limit='{}'".format(rate_limit))
# Prune from recent_send_times anything more than a few seconds
# ago. Even though SES reports a maximum per-second, the way
# they enforce the limit may not be on a one-second window.
# To be safe, we use a two-second window (but allow 2 times the
# rate limit) and then also have a default rate limit factor of
# 0.5 so that we really limit the one-second amount in two
# seconds.
window = 2.0 # seconds
window_start = now - timedelta(seconds=window)
new_send_times = []
for time in recent_send_times:
if time > window_start:
new_send_times.append(time)
recent_send_times = new_send_times
# If the number of recent send times in the last 1/_throttle
# seconds exceeds the rate limit, add a delay.
# Since I'm not sure how Amazon determines at exactly what
# point to throttle, better be safe than sorry and let in, say,
# half of the allowed rate.
if len(new_send_times) > rate_limit * window * self._throttle:
# Sleep the remainder of the window period.
delta = now - new_send_times[0]
total_seconds = (delta.microseconds + (delta.seconds +
delta.days * 24 * 3600) * 10**6) / 10**6
delay = window - total_seconds
if delay > 0:
sleep(delay)
recent_send_times.append(now)
# end of throttling
try:
logger.info("Try to send raw email")
response = self.connection.send_raw_email(
Source=source or message.from_email,
Destinations=message.recipients(),
# todo attachments?
RawMessage={'Data': dkim_sign(message.message().as_string(),
dkim_key=self.dkim_key,
dkim_domain=self.dkim_domain,
dkim_selector=self.dkim_selector,
dkim_headers=self.dkim_headers)}
)
message.extra_headers['status'] = 200
message.extra_headers['message_id'] = response['MessageId']
message.extra_headers['request_id'] = response['ResponseMetadata']['RequestId']
num_sent += 1
if 'X-SES-CONFIGURATION-SET' in message.extra_headers:
logger.debug(
u"send_messages.sent from='{}' recipients='{}' message_id='{}' request_id='{}' "
u"ses-configuration-set='{}'".format(
message.from_email,
", ".join(message.recipients()),
message.extra_headers['message_id'],
message.extra_headers['request_id'],
message.extra_headers['X-SES-CONFIGURATION-SET']
))
else:
logger.debug(u"send_messages.sent from='{}' recipients='{}' message_id='{}' request_id='{}'".format(
message.from_email,
", ".join(message.recipients()),
message.extra_headers['message_id'],
message.extra_headers['request_id']
))
except ResponseError as err:
# Store failure information so to post process it if required
error_keys = ['status', 'reason', 'body', 'request_id',
'error_code', 'error_message']
for key in error_keys:
message.extra_headers[key] = getattr(err, key, None)
if not self.fail_silently:
raise
if new_conn_created:
self.close()
return num_sent
def get_rate_limit(self):
if self._access_key_id in cached_rate_limits:
return cached_rate_limits[self._access_key_id]
new_conn_created = self.open()
if not self.connection:
raise Exception(
"No connection is available to check current SES rate limit.")
try:
quota_dict = self.connection.get_send_quota()
max_per_second = quota_dict['MaxSendRate']
ret = float(max_per_second)
cached_rate_limits[self._access_key_id] = ret
return ret
finally:
if new_conn_created:
self.close()

136
django_aws_ses/models.py Normal file
View File

@ -0,0 +1,136 @@
import hashlib
import logging
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.sites.models import Site
from django.contrib.auth import get_user_model # If used custom user model
from django.urls import reverse
from django.utils.encoding import force_bytes, force_text
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
User = get_user_model()
class AwsSesSettings(models.Model):
site = models.OneToOneField(Site, on_delete=models.CASCADE)
access_key = models.CharField(max_length=255, blank=True, null=True,)
secret_key = models.CharField(max_length=255, blank=True, null=True,)
region_name = models.CharField(max_length=255, blank=True, null=True,)
region_endpoint = models.CharField(max_length=255, blank=True, null=True,)
class Meta:
verbose_name = 'AWS SES Settings'
@receiver(post_save, sender=Site)
def update_awsses_settings(sender, instance, created, **kwargs):
if created:
AwsSesSettings.objects.create(Site=instance)
instance.AwsSesSettings.save()
class AwsSesUserAddon(models.Model):
user = models.OneToOneField(User, related_name='aws_ses', on_delete=models.CASCADE)
unsubscribe = models.BooleanField(default=False)
class Meta:
verbose_name = 'User Data'
def get_email(self):
email_field = self.user.get_email_field_name()
email = getattr(self, email_field, '') or ''
return email
def unsubscribe_hash_generator(self):
email = self.get_email()
string_to_hash = "%s%s" % (str(self.user.pk), email)
return hashlib.md5(string_to_hash.encode()).hexdigest()
def check_unsubscribe_hash(self, hash):
test_hash = self.unsubscribe_hash_generator()
return hash == test_hash
def unsubscribe_url_generator(self):
uuid = urlsafe_base64_encode(force_bytes(self.user.pk))
hash = self.unsubscribe_hash_generator()
return reverse('django_aws_ses:aws_ses_unsubscribe', kwargs={"uuid":uuid, "hash":hash})
@receiver(post_save, sender=User)
def update_awsses_user(sender, instance, created, **kwargs):
if created:
AwsSesUserAddon.objects.create(user=instance)
try:
instance.AwsSesUserAddon.save()
except AwsSesUserAddon.DoesNotExist:
AwsSesUserAddon.objects.create(user=instance)
class SESStat(models.Model):
date = models.DateField(unique=True, db_index=True)
delivery_attempts = models.PositiveIntegerField()
bounces = models.PositiveIntegerField()
complaints = models.PositiveIntegerField()
rejects = models.PositiveIntegerField()
class Meta:
verbose_name = 'SES Stat'
ordering = ['-date']
def __unicode__(self):
return self.date.strftime("%Y-%m-%d")
class BounceRecord(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
email = models.EmailField()
bounce_type = models.CharField(max_length=255, blank=True, null=True,)
bounce_sub_type = models.CharField(max_length=255, blank=True, null=True,)
reporting_mta = models.CharField(max_length=255, blank=True, null=True,)
status = models.CharField(max_length=255, blank=True, null=True,)
action = models.CharField(max_length=255, blank=True, null=True,)
feedback_id = models.CharField(max_length=255, blank=True, null=True,)
diagnostic_code = models.CharField(max_length=255, blank=True, null=True,)
cleared = models.BooleanField(default=False)
def __str__(self):
return "email: %s, type: %s, sub_type: %s, status: %s, date: %s" % (self.email, self.bounce_type, self.bounce_sub_type, self.status, self.timestamp)
class ComplaintRecord(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
email = models.EmailField()
sub_type = models.CharField(max_length=255, blank=True, null=True,)
feedback_id = models.CharField(max_length=255, blank=True, null=True,)
feedback_type = models.CharField(max_length=255, blank=True, null=True,)
def __str__(self):
return "email: %s, sub_type: %s, feedback_type: %s, date: %s" % (self.email, self.bounce_sub_type, self.feedback_type, self.timestamp)
class SendRecord(models.Model):
SEND = 'Send'
DELIVERED = 'Delivery'
STATUS_CHOICE = (
(SEND, SEND),
(DELIVERED, DELIVERED),
)
timestamp = models.DateTimeField(auto_now_add=True)
source = models.EmailField()
destination = models.EmailField()
subject = models.CharField(max_length=255, blank=True, null=True,)
message_id = models.CharField(max_length=255, blank=True, null=True,)
aws_process_time = models.IntegerField()
smtp_response = models.CharField(max_length=255, blank=True, null=True,)
status = models.CharField(max_length=255, blank=True, null=True,)
def __str__(self):
return "source: %s, destination: %s, subject: %s, date: %s" % (self.source, self.destination, self.subject, self.timestamp)
class UnknownRecord(models.Model):
timestamp = models.DateTimeField(auto_now_add=True)
event_type = models.CharField(max_length=255, blank=True, null=True,)
aws_data = models.TextField(blank=True, null=True,)
def __str__(self):
return "eventType: %s, timestamp: %s" % (self.eventType, self.timestamp)

View File

@ -0,0 +1,82 @@
from django.conf import settings
import logging
from .models import (
AwsSesSettings
)
aws_ses_Settings, c = AwsSesSettings.objects.get_or_create(site_id=settings.SITE_ID)
__all__ = ('ACCESS_KEY', 'SECRET_KEY', 'AWS_SES_REGION_NAME',
'AWS_SES_REGION_ENDPOINT', 'AWS_SES_AUTO_THROTTLE',
'AWS_SES_RETURN_PATH', 'DKIM_DOMAIN', 'DKIM_PRIVATE_KEY',
'DKIM_SELECTOR', 'DKIM_HEADERS', 'TIME_ZONE', 'BASE_DIR',
'BOUNCE_LIMIT','SES_BACKEND_DEBUG','SES_BACKEND_DEBUG_LOGFILE_PATH',
'SES_BACKEND_DEBUG_LOGFILE_FORMATTER')
BASE_DIR = getattr(settings, 'BASE_DIR', None)
if not BASE_DIR:
raise RuntimeError('No BASE_DIR defined in project settings, django_aws_ses requires BASE_DIR to be defined and pointed at your root directory. i.e. BASE_DIR = os.path.dirname(os.path.abspath(__file__))')
DEFAULT_FROM_EMAIL = getattr(settings, 'DEFAULT_FROM_EMAIL', 'no_reply@%s' % aws_ses_Settings.site.domain)
HOME_URL = getattr(settings, 'HOME_URL', '')
UNSUBSCRIBE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/unsebscribe.html')
BASE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/base.html')
ACCESS_KEY = aws_ses_Settings.access_key or getattr(settings, 'AWS_SES_ACCESS_KEY_ID',getattr(settings, 'AWS_ACCESS_KEY_ID', None))
SECRET_KEY = aws_ses_Settings.secret_key or getattr(settings, 'AWS_SES_SECRET_ACCESS_KEY',getattr(settings, 'AWS_SECRET_ACCESS_KEY', None))
AWS_SES_REGION_NAME = aws_ses_Settings.region_name or getattr(settings, 'AWS_SES_REGION_NAME',getattr(settings, 'AWS_DEFAULT_REGION', 'us-east-1'))
AWS_SES_REGION_ENDPOINT = aws_ses_Settings.region_endpoint or getattr(settings, 'AWS_SES_REGION_ENDPOINT','email.us-east-1.amazonaws.com')
AWS_SES_REGION_ENDPOINT_URL = getattr(settings, 'AWS_SES_REGION_ENDPOINT_URL','https://' + AWS_SES_REGION_ENDPOINT)
AWS_SES_AUTO_THROTTLE = getattr(settings, 'AWS_SES_AUTO_THROTTLE', 0.5)
AWS_SES_RETURN_PATH = getattr(settings, 'AWS_SES_RETURN_PATH', None)
AWS_SES_CONFIGURATION_SET = getattr(settings, 'AWS_SES_CONFIGURATION_SET', None)
DKIM_DOMAIN = getattr(settings, "DKIM_DOMAIN", None)
DKIM_PRIVATE_KEY = getattr(settings, 'DKIM_PRIVATE_KEY', None)
DKIM_SELECTOR = getattr(settings, 'DKIM_SELECTOR', 'ses')
DKIM_HEADERS = getattr(settings, 'DKIM_HEADERS',
('From', 'To', 'Cc', 'Subject'))
TIME_ZONE = settings.TIME_ZONE
VERIFY_BOUNCE_SIGNATURES = getattr(settings, 'AWS_SES_VERIFY_BOUNCE_SIGNATURES', True)
# Domains that are trusted when retrieving the certificate
# used to sign bounce messages.
BOUNCE_CERT_DOMAINS = getattr(settings, 'AWS_SNS_BOUNCE_CERT_TRUSTED_DOMAINS', (
'amazonaws.com',
'amazon.com',
))
SES_BOUNCE_LIMIT = getattr(settings,'BOUNCE_LIMT', 1)
SES_BACKEND_DEBUG = getattr(settings,'SES_BACKEND_DEBUG', False)
SES_BACKEND_DEBUG_LOGFILE_PATH = getattr(settings,'SES_BACKEND_DEBUG_LOGFILE_PATH', '%s/aws_ses.log' % BASE_DIR)
SES_BACKEND_DEBUG_LOGFILE_FORMATTER = getattr(settings,'SES_BACKEND_DEBUG_LOGFILE_FORMATTER', '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('django_aws_ses')
# logger.setLevel(logging.WARNING)
if SES_BACKEND_DEBUG:
logger.setLevel(logging.INFO)
# create a file handler
if SES_BACKEND_DEBUG_LOGFILE_PATH:
handler = logging.FileHandler(SES_BACKEND_DEBUG_LOGFILE_PATH)
handler.setLevel(logging.INFO)
# create a logging format
formatter = logging.Formatter(SES_BACKEND_DEBUG_LOGFILE_FORMATTER)
handler.setFormatter(formatter)
# add the handlers to the logger
logger.addHandler(handler)
#logger.info('something we are logging')

View File

@ -0,0 +1,8 @@
from django.dispatch import Signal
# The following fields are used from the 3 signals below: mail_obj, bounce_obj, raw_message
bounce_received = Signal()
complaint_received = Signal()
delivery_received = Signal()
email_pre_send = Signal()
email_post_send = Signal()

View File

@ -0,0 +1,14 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title1 %}N/A {% endblock title1 %} | {% block title2 %} {{ site.domain }}{% endblock title2 %}</title>
</head>
<body>
<div id="body_contaner" class=" cardContenet">
{% block content %}
if you see this, something is wrong!
{% endblock content %}
</div>
</body>
</html>

View File

@ -0,0 +1,144 @@
{% extends "admin/base_site.html" %}
{% block extrastyle %}
{{ block.super }}
<style>table {width: 100%;}</style>
{% endblock %}
{% block extrahead %}
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawChart);
function drawChart() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Time');
data.addColumn('number', 'Delivery Attempts');
data.addColumn('number', 'Bounces');
data.addColumn('number', 'Complaints');
data.addColumn('number', 'Rejected');
data.addRows({{ datapoints|length }});
{% for datapoint in datapoints %}
data.setValue({{ forloop.counter0 }}, 0, {% if local_time %}'{{ datapoint.Timestamp }}'{% else %}'{{ datapoint.Timestamp|slice:"11:19" }} {{ datapoint.Timestamp|slice:":10" }}'{% endif %});
data.setValue({{ forloop.counter0 }}, 1, {{ datapoint.DeliveryAttempts }});
data.setValue({{ forloop.counter0 }}, 2, {{ datapoint.Bounces }});
data.setValue({{ forloop.counter0 }}, 3, {{ datapoint.Complaints }});
data.setValue({{ forloop.counter0 }}, 4, {{ datapoint.Rejects }});
{% endfor %}
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(data, {
width: 498,
height: 300,
title: 'Sending Stats',
hAxis: {textPosition: 'none'},
chartArea: {left:30,top:30,width:460,height:230},
legend: 'bottom'
});
}
</script>
{% endblock %}
{% block bodyclass %}dashboard{% endblock %}
{% block content_title %}<h1>SES Stats</h1>{% endblock %}
{% block content %}
<p>Access Key: <span id="aws_access_key_id">{{ access_key }}</span></p>
<div id="content-main">
<div class="module">
<table id="quota">
<caption>Quotas</caption>
<thead>
<tr>
<th>24 Quota</th>
<th>24 Sent</th>
<th>Quota Remaining</th>
<th>Per/s Quota</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ 24hour_quota }}</td>
<td>{{ 24hour_sent }}</td>
<td>{{ 24hour_remaining }}</td>
<td>{{ persecond_rate }}</td>
</tr>
</tbody>
</table>
</div>
<div class="module">
<table id="sending_totals">
<caption>Sending Stats</caption>
<thead>
<tr>
<th>Delivery Attempts</th>
<th>Bounces</th>
<th>Complaints</th>
<th>Rejected</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ summary.DeliveryAttempts }}</td>
<td>{{ summary.Bounces }}</td>
<td>{{ summary.Complaints }}</td>
<td>{{ summary.Rejects }}</td>
</tr>
</tbody>
</table>
<div id="chart"></div>
</div>
<div class="module">
<table id="sending_stats">
<caption>Sending Activity</caption>
<thead>
<tr>
<th style="width:35px">Delivery Attempts</th>
<th>Bounces</th>
<th>Complaints</th>
<th>Rejected</th>
<th>{% if local_time %}Local Time{% else %}Timestamp{% endif %}</th>
</tr>
</thead>
<tbody>
{% for datapoint in datapoints %}
<tr>
<td>{{ datapoint.DeliveryAttempts }}</td>
<td>{{ datapoint.Bounces }}</td>
<td>{{ datapoint.Complaints }}</td>
<td>{{ datapoint.Rejects }}</td>
<td>{{ datapoint.Timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block sidebar %}
<div id="content-related">
<div class="module" id="recent-actions-module">
<h2>Verified Emails</h2>
<table id="verified_emails">
<thead>
<tr>
<th>Email Address</th>
</tr>
</thead>
<tbody>
{% for email_address in verified_emails %}
<tr>
<td>{{ email_address }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr><td><strong>{{ verified_emails|length }}</strong></td></tr>
</tfoot>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends base_template_name %}
{% block title1 %}
Unsubscribe
{% endblock title1 %}
{% block content %}
<div>
<h3>{{ unsubscribe_message }}</h3>
</div>
{% endblock content %}

17
django_aws_ses/urls.py Normal file
View File

@ -0,0 +1,17 @@
from django.conf.urls import include, url
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from .views import (
dashboard,
handle_bounce,
HandleUnsubscribe
)
app_name = "django_aws_ses"
urlpatterns = [
url(r'^status/$', dashboard, name='aws_ses_status'),
url(r'^bounce/$', csrf_exempt(handle_bounce),name='aws_ses_bounce'),
url(r'^unsubscribe/(?P<uuid>[0-9a-zA-Z]+)/(?P<hash>[0-9a-zA-Z]+)/$', HandleUnsubscribe.as_view(), name='aws_ses_unsubscribe')
]

280
django_aws_ses/utils.py Normal file
View File

@ -0,0 +1,280 @@
import base64
import logging
from builtins import str as text
from builtins import bytes
from io import StringIO
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import smart_str
from django.dispatch.dispatcher import receiver
from django.db.models import Count
from django.contrib.auth import get_user_model # If used custom user model
User = get_user_model()
from . import settings
from . import signals
from . import utils
from .models import (
BounceRecord,
ComplaintRecord
)
logger = settings.logger
class BounceMessageVerifier(object):
"""
A utility class for validating bounce messages
See: http://docs.amazonwebservices.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
"""
def __init__(self, bounce_dict):
"""
Creates a new bounce message from the given dict.
"""
self._data = bounce_dict
self._verified = None
def is_verified(self):
"""
Verifies an SES bounce message.
"""
if self._verified is None:
signature = self._data.get('Signature')
if not signature:
self._verified = False
return self._verified
# Decode the signature from base64
signature = bytes(base64.b64decode(signature))
# Get the message to sign
sign_bytes = self._get_bytes_to_sign()
if not sign_bytes:
self._verified = False
return self._verified
if not self.certificate:
self._verified = False
return self._verified
# Extract the public key
pkey = self.certificate.get_pubkey()
# Use the public key to verify the signature.
pkey.verify_init()
pkey.verify_update(sign_bytes)
verify_result = pkey.verify_final(signature)
self._verified = verify_result == 1
return self._verified
@property
def certificate(self):
"""
Retrieves the certificate used to sign the bounce message.
TODO: Cache the certificate based on the cert URL so we don't have to
retrieve it for each bounce message. *We would need to do it in a
secure way so that the cert couldn't be overwritten in the cache*
"""
if not hasattr(self, '_certificate'):
cert_url = self._get_cert_url()
# Only load certificates from a certain domain?
# Without some kind of trusted domain check, any old joe could
# craft a bounce message and sign it using his own certificate
# and we would happily load and verify it.
if not cert_url:
self._certificate = None
return self._certificate
try:
import requests
except ImportError:
raise ImproperlyConfigured(
"`requests` is required for bounce message verification. "
"Please consider installing the `django-ses` with the "
"`bounce` extra - e.g. `pip install django-ses[bounce]`."
)
try:
import M2Crypto
except ImportError:
raise ImproperlyConfigured(
"`M2Crypto` is required for bounce message verification. "
"Please consider installing the `django-ses` with the "
"`bounce` extra - e.g. `pip install django-ses[bounce]`."
)
# We use requests because it verifies the https certificate
# when retrieving the signing certificate. If https was somehow
# hijacked then all bets are off.
response = requests.get(cert_url)
if response.status_code != 200:
logger.warning(u'Could not download certificate from %s: "%s"', cert_url, response.status_code)
self._certificate = None
return self._certificate
# Handle errors loading the certificate.
# If the certificate is invalid then return
# false as we couldn't verify the message.
try:
self._certificate = M2Crypto.X509.load_cert_string(response.content)
except M2Crypto.X509.X509Error as e:
logger.warning(u'Could not load certificate from %s: "%s"', cert_url, e)
self._certificate = None
return self._certificate
def _get_cert_url(self):
"""
Get the signing certificate URL.
Only accept urls that match the domains set in the
AWS_SNS_BOUNCE_CERT_TRUSTED_DOMAINS setting. Sub-domains
are allowed. i.e. if amazonaws.com is in the trusted domains
then sns.us-east-1.amazonaws.com will match.
"""
cert_url = self._data.get('SigningCertURL')
if cert_url:
if cert_url.startswith('https://'):
url_obj = urlparse(cert_url)
for trusted_domain in settings.BOUNCE_CERT_DOMAINS:
parts = trusted_domain.split('.')
if url_obj.netloc.split('.')[-len(parts):] == parts:
return cert_url
logger.warning(u'Untrusted certificate URL: "%s"', cert_url)
else:
logger.warning(u'No signing certificate URL: "%s"', cert_url)
return None
def _get_bytes_to_sign(self):
"""
Creates the message used for signing SNS notifications.
This is used to verify the bounce message when it is received.
"""
# Depending on the message type the fields to add to the message
# differ so we handle that here.
msg_type = self._data.get('Type')
if msg_type == 'Notification':
fields_to_sign = [
'Message',
'MessageId',
'Subject',
'Timestamp',
'TopicArn',
'Type',
]
elif (msg_type == 'SubscriptionConfirmation' or
msg_type == 'UnsubscribeConfirmation'):
fields_to_sign = [
'Message',
'MessageId',
'SubscribeURL',
'Timestamp',
'Token',
'TopicArn',
'Type',
]
else:
# Unrecognized type
logger.warning(u'Unrecognized SNS message Type: "%s"', msg_type)
return None
outbytes = StringIO()
for field_name in fields_to_sign:
field_value = smart_str(self._data.get(field_name, ''),
errors="replace")
if field_value:
outbytes.write(text(field_name))
outbytes.write(text("\n"))
outbytes.write(text(field_value))
outbytes.write(text("\n"))
response = outbytes.getvalue()
return bytes(response, 'utf-8')
def verify_bounce_message(msg):
"""
Verify an SES/SNS bounce notification message.
"""
verifier = BounceMessageVerifier(msg)
return verifier.is_verified()
@receiver(signals.email_pre_send)
def receiver_email_pre_send(sender, message=None, **kwargs):
logger.info("receiver_email_pre_send received signal")
def filter_recipiants(recipiant_list):
if len(recipiant_list) > 0:
recipiant_list = filter_recipiants_with_unsubscribe(recipiant_list)
if len(recipiant_list) > 0:
recipiant_list = filter_recipiants_with_complaint_records(recipiant_list)
if len(recipiant_list) > 0:
recipiant_list = filter_recipiants_with_bounce_records(recipiant_list)
return recipiant_list
def filter_recipiants_with_unsubscribe(recipiant_list):
"""
filter message recipiants so we don't send emails to any email that have Unsubscribude
"""
logger.info("unsubscribe filter running")
logger.info("message.recipients() befor blacklist_emails filter: %s" % recipiant_list)
blacklist_emails = list(set([record.email for record in User.objects.filter(aws_ses__unsubscribe=True)]))
if blacklist_emails:
return filter_recipiants_with_blacklist(recipiant_list, blacklist_emails)
else:
return recipiant_list
def filter_recipiants_with_complaint_records(recipiant_list):
"""
filter message recipiants so we don't send emails to any email that have a ComplaintRecord
"""
logger.info("complaint_records filter running")
logger.info("message.recipients() befor blacklist_emails filter: %s" % recipiant_list)
blacklist_emails = list(set([record.email for record in ComplaintRecord.objects.filter(email__isnull=False)]))
if blacklist_emails:
return filter_recipiants_with_blacklist(recipiant_list, blacklist_emails)
else:
return recipiant_list
def filter_recipiants_with_bounce_records(recipiant_list):
"""
filter message recipiants so we dont send emails to any email that has more BounceRecord
the SES_BOUNCE_LIMIT
"""
logger.info("bounce_records filter running")
logger.info("message.recipients() befor blacklist_emails filter: %s" % recipiant_list)
blacklist_emails = list(set([record.email for record in BounceRecord.objects.filter(email__isnull=False).annotate(total=Count('email')).filter(total__gte=settings.SES_BOUNCE_LIMIT)]))
if blacklist_emails:
return filter_recipiants_with_blacklist(recipiant_list, blacklist_emails)
else:
return recipiant_list
def filter_recipiants_with_blacklist(recipiant_list, blacklist_emails):
"""
filter message recipiants with a list of email you dont want to email
"""
logger.info("blacklist_emails filter list: %s" % blacklist_emails)
filtered_recipiant_list = [email for email in recipiant_list if email not in blacklist_emails]
logger.info("filtered_recipiant_list: %s" % filtered_recipiant_list)
return filtered_recipiant_list

454
django_aws_ses/views.py Normal file
View File

@ -0,0 +1,454 @@
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
from datetime import datetime
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
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_text
from . import settings
from . import signals
from . import utils
from .models import (
BounceRecord,
ComplaintRecord,
SendRecord,
UnknownRecord,
AwsSesUserAddon
)
logger = settings.logger
User = get_user_model()
def superuser_only(view_func):
"""
Limit a view to superuser only.
"""
def _inner(request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied
return view_func(request, *args, **kwargs)
return _inner
def stats_to_list(stats_dict, localize=pytz):
"""
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 = []
if localize:
current_tz = localize.timezone(settings.TIME_ZONE)
else:
current_tz = None
for dp in result['SendDataPoints']:
if current_tz:
utc_dt = dp['Timestamp']
dp['Timestamp'] = current_tz.normalize(
utc_dt.astimezone(current_tz))
datapoints.append(dp)
datapoints.sort(key=lambda x: x['Timestamp'])
return datapoints
def emails_parse(emails_dict):
"""
Parse the output of ``SESConnection.list_verified_emails()`` and get
a list of emails.
"""
return sorted([email for email in emails_dict['VerifiedEmailAddresses']])
def sum_stats(stats_data):
"""
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 {
'Bounces': t_bounces,
'Complaints': t_complaints,
'DeliveryAttempts': t_delivery_attempts,
'Rejects': t_rejects,
}
@superuser_only
def dashboard(request):
"""
Graph SES send statistics over time.
"""
cache_key = 'vhash:django_aws_ses_status'
cached_view = cache.get(cache_key)
if cached_view:
return cached_view
ses_conn = boto3.client(
'ses',
aws_access_key_id=settings.ACCESS_KEY,
aws_secret_access_key=settings.SECRET_KEY,
region_name=settings.AWS_SES_REGION_NAME,
endpoint_url=settings.AWS_SES_REGION_ENDPOINT_URL,
)
quota_dict = ses_conn.get_send_quota()
verified_emails_dict = ses_conn.list_verified_email_addresses()
stats = ses_conn.get_send_statistics()
verified_emails = emails_parse(verified_emails_dict)
ordered_data = stats_to_list(stats)
summary = sum_stats(ordered_data)
extra_context = {
'title': 'SES Statistics',
'datapoints': ordered_data,
'24hour_quota': quota_dict['Max24HourSend'],
'24hour_sent': quota_dict['SentLast24Hours'],
'24hour_remaining':
quota_dict['Max24HourSend'] -
quota_dict['SentLast24Hours'],
'persecond_rate': quota_dict['MaxSendRate'],
'verified_emails': verified_emails,
'summary': summary,
'access_key': settings.ACCESS_KEY,
'local_time': True,
}
response = render(request, 'django_aws_ses/send_stats.html', extra_context)
cache.set(cache_key, response, 60 * 15) # Cache for 15 minutes
return response
@require_POST
def handle_bounce(request):
"""
Handle a bounced email via an SNS webhook.
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:
notification = json.loads(raw_json.decode('utf-8'))
except ValueError as e:
# TODO: What kind of response should be returned here?
logger.warning(u'Received bounce with bad JSON: "%s"', e)
return HttpResponseBadRequest()
# Verify the authenticity of the bounce message.
if (settings.VERIFY_BOUNCE_SIGNATURES and
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()
logger.info('notification.get("Type"): %s' % notification.get("Type"))
if notification.get('Type') in ('SubscriptionConfirmation',
'UnsubscribeConfirmation'):
# Process the (un)subscription confirmation.
logger.info(
u'Received subscription confirmation: TopicArn: %s',
notification.get('TopicArn'),
extra={
'notification': notification,
},
)
# Get the subscribe url and hit the url to confirm the subscription.
subscribe_url = notification.get('SubscribeURL')
try:
urlopen(subscribe_url).read()
except URLError as e:
# Some kind of error occurred when confirming the request.
logger.error(
u'Could not confirm subscription: "%s"', e,
extra={
'notification': notification,
},
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:
mail_obj = message.get('mail')
event_type = message.get('notificationType', message.get('eventType'))
logger.info('event_type: %s' % event_type)
if event_type == 'Bounce':
# Bounce
bounce_obj = message.get('bounce', {})
# Logging
feedback_id = bounce_obj.get('feedbackId')
bounce_type = bounce_obj.get('bounceType')
bounce_subtype = bounce_obj.get('bounceSubType')
bounce_recipients = bounce_obj.get('bouncedRecipients', [])
# create a BounceRecord so we can keep from sending to bad emails.
logger.info('create records')
for recipient in bounce_recipients:
logger.info('recipient: %s' % recipient)
BounceRecord.objects.create(
email = recipient.get('emailAddress', None),
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),
)
logger.info(
u'Received bounce notification: feedbackId: %s, bounceType: %s, bounceSubType: %s',
feedback_id, bounce_type, bounce_subtype,
extra={
'notification': notification,
},
)
signals.bounce_received.send(
sender=handle_bounce,
mail_obj=mail_obj,
bounce_obj=bounce_obj,
raw_message=raw_json,
)
elif event_type == 'Complaint':
# Complaint
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, feedbackType: %s',
message_id, feedback_type,
extra={
'notification': notification,
},
)
signals.delivery_received.send(
sender=handle_bounce,
mail_obj=mail_obj,
delivery_obj=delivery_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(
u'Received unknown notification type: %s',
notification.get('Type'),
extra={
'notification': notification,
},
)
# AWS will consider anything other than 200 to be an error response and
# resend the SNS request. We don't need that so we return 200 here.
return HttpResponse()
class HandleUnsubscribe(TemplateView):
http_method_names = ['get']
template_name = settings.UNSUBSCRIBE_TEMPLET
base_template_name = settings.BASE_TEMPLET
unsubscribe_message = "We Have Unsubscribed the Following Email"
def get_context_data(self, **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['unsubscribe_message'] = self.unsubscribe_message
return context
def get(self, request, *args, **kwargs):
uuid = self.kwargs['uuid']
hash = self.kwargs['hash']
logger.info("in get ----- self.base_template_name: %s" % self.base_template_name)
try:
uuid = force_text(urlsafe_base64_decode(uuid).decode())
logger.info('uuid: %s' % uuid)
user = User.objects.get(pk=uuid)
logger.info('user.pk: %s' % user.pk)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return redirect(settings.HOME_URL)
try:
ses = user.aws_ses
except AwsSesUserAddon.DoesNotExist:
ses = AwsSesUserAddon.objects.create(user=user)
if user is not None and user.aws_ses.check_unsubscribe_hash(hash):
logger.info('ses.pk: %s' % ses.pk)
ses.unsubscribe = True
ses.save()
else:
logger.warning("bad hash was provided!")
return redirect(settings.HOME_URL)
return super(HandleUnsubscribe, self).get(request, *args, **kwargs)