From 823d6c1b2dd11d40936620336fbb97b1146690cc Mon Sep 17 00:00:00 2001 From: Raymond Jessop Date: Tue, 7 Nov 2023 19:32:25 +0000 Subject: [PATCH] bringing the repo up to date with the code git-svn-id: https://vault.zeeksgeeks.com/svn/django_aws_ses/trunk@6 ed966f06-d3d6-432b-bc91-693151a5c6b4 --- django_aws_ses/admin.py | 16 +++- django_aws_ses/apps.py | 1 + django_aws_ses/backends.py | 107 ++++++++++++++++++++------ django_aws_ses/models.py | 30 ++++++-- django_aws_ses/settings.py | 149 +++++++++++++++++++++---------------- django_aws_ses/utils.py | 118 ++++++++++++++++++++++++----- django_aws_ses/views.py | 14 ++-- 7 files changed, 316 insertions(+), 119 deletions(-) diff --git a/django_aws_ses/admin.py b/django_aws_ses/admin.py index f89b316..9f69ce7 100644 --- a/django_aws_ses/admin.py +++ b/django_aws_ses/admin.py @@ -6,7 +6,8 @@ from .models import ( AwsSesUserAddon, ComplaintRecord, SendRecord, - UnknownRecord + UnknownRecord, + BlackListedDomains, ) from . import settings @@ -62,15 +63,22 @@ class ComplaintRecordAdmin(admin.ModelAdmin): admin.site.register(ComplaintRecord, ComplaintRecordAdmin) class SendRecordAdmin(admin.ModelAdmin): - model = ComplaintRecord + model = SendRecord 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 + model = UnknownRecord list_display = ('event_type', 'aws_data') list_filter = ('event_type', 'aws_data') -admin.site.register(UnknownRecord, UnknownRecordAdmin) \ No newline at end of file +admin.site.register(UnknownRecord, UnknownRecordAdmin) + +class BlackListedDomainsAdmin(admin.ModelAdmin): + model = BlackListedDomains + list_display = ('domain', 'timestamp') + list_filter = ('domain', 'timestamp') + +admin.site.register(BlackListedDomains, BlackListedDomainsAdmin) \ No newline at end of file diff --git a/django_aws_ses/apps.py b/django_aws_ses/apps.py index 7129528..c44ea11 100644 --- a/django_aws_ses/apps.py +++ b/django_aws_ses/apps.py @@ -2,5 +2,6 @@ from django.apps import AppConfig class DjangoAwsSesBackendConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' name = 'django_aws_ses' verbose_name = 'Django AWS SES' diff --git a/django_aws_ses/backends.py b/django_aws_ses/backends.py index f7f94ef..a96d02a 100644 --- a/django_aws_ses/backends.py +++ b/django_aws_ses/backends.py @@ -13,7 +13,8 @@ import sys from . import settings from . import signals from . import utils -from .models import BounceRecord +from .models import BounceRecord +from saleor.core.utils import email_exclusion_filter logger = settings.logger @@ -82,6 +83,8 @@ class SESBackend(BaseEmailBackend): except Exception: if not self.fail_silently: raise + + return True def close(self): """Close any open HTTP connections to the API server. @@ -90,10 +93,13 @@ class SESBackend(BaseEmailBackend): def send_messages(self, email_messages): """Sends one or more EmailMessage objects and returns the number of - email messages sent. + email messages sent and a list of filtered emails. """ - - + logger.info("1 --- start of send_messages") + list_of_response = [] + num_sent = 0 + not_sent_list = [] + sent_message = {"Sent":""} calling_func = '' try: fcount = 0 @@ -103,22 +109,31 @@ class SESBackend(BaseEmailBackend): calling_func = sys._getframe(fcount).f_code.co_name except Exception as e: - logger.info("fcount:%s, called from exception = %s" , (fcount, e)) + logger.info("fcount:%s, called from exception = %s" % (fcount, e)) - logger.info("called from %s" , (calling_func)) + logger.info("called from %s current throttle:%s" % (calling_func, self._throttle)) logger.info("send_messages") if not email_messages: - return + logger.info("no email messages returning") + list_of_response.append({'error':'no email messages returning'}) new_conn_created = self.open() + if new_conn_created: + logger.info("created a new connection") + if not self.connection: # Failed silently - return + logger.info("no connection returning") + list_of_response.append({'error':'no connection returning'}) + logger.info("DEBUGING EMAILS --- list_of_response:%s" % (list_of_response)) + logger.info("DEBUGING EMAILS --- return %s" % (num_sent)) + return num_sent - num_sent = 0 + + source = settings.AWS_SES_RETURN_PATH logger.info("email_messages: %s" % email_messages) @@ -131,16 +146,43 @@ class SESBackend(BaseEmailBackend): # 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. + message.aws_ses_response = {'error':'not sent yet'} + logger.info("Sending signal(email_pre_send)") - signals.email_pre_send.send_robust(self.__class__, message=message) + logger.info("message to: %s, cc: %s, bcc: %s" % (message.to, message.cc, message.bcc)) + signals.email_pre_send.send_robust(self.__class__, message=message) + + # for log in dir(message): + # logger.info(log) + pre_filter_recipients = message.recipients() + logger.info("message.recipients() = %s" % message.recipients()) + + marketing = message.extra_headers.get("marketing","False") + message.to = email_exclusion_filter(message.to,marketing) + message.cc = email_exclusion_filter(message.cc,marketing) + message.bcc = email_exclusion_filter(message.bcc,marketing) - message.to = utils.filter_recipiants(message.recipients()) + message.to = utils.filter_recipiants(message.to) + message.cc = utils.filter_recipiants(message.cc) + message.bcc = utils.filter_recipiants(message.bcc) 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 + list_of_response.append({'error':'no recipients left after the filter'}) + message.aws_ses_response = {'error':'no recipients left after the filter'} + sent_message = {"Not Sent":"No recipients left after filters"} + continue + + #raise Exception('No emails left after filters!') + else: + for email in pre_filter_recipients: + if email not in message.recipients(): + not_sent_list.append(email) if (settings.AWS_SES_CONFIGURATION_SET and 'X-SES-CONFIGURATION-SET' not in message.extra_headers): @@ -164,13 +206,16 @@ class SESBackend(BaseEmailBackend): # Set the setting to 0 or None to disable throttling. if self._throttle: global recent_send_times + logger.info("inside if _throttle recent_send_times:%s" % recent_send_times) now = datetime.now() + logger.info("inside if _throttle now:%s" % now) # Get and cache the current SES max-per-second rate limit # returned by the SES API. + logger.info("inside if _throttle calling get_rate_limit") rate_limit = self.get_rate_limit() - logger.debug(u"send_messages.throttle rate_limit='{}'".format(rate_limit)) + logger.info("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 @@ -208,7 +253,7 @@ class SESBackend(BaseEmailBackend): logger.info("Try to send raw email") #logger.info('message.message().as_string() = %s' % message.message().as_string()) logger.info("source = %s" % source) - logger.info("message.from_email = %s" % self.dkim_key) + logger.info("message.from_email = %s" % message.from_email) logger.info("message.recipients() = %s" % message.recipients()) logger.info("dkim_key = %s" % self.dkim_key) @@ -221,16 +266,21 @@ class SESBackend(BaseEmailBackend): # todo attachments? RawMessage={'Data': dkim_sign(message.message().as_string(), dkim_key=self.dkim_key, - dkim_domain=self.dkim_domain, + dkim_domain=self.dkim_domain, dkim_selector=self.dkim_selector, dkim_headers=self.dkim_headers)} ) + + list_of_response.append(response) + + message.aws_ses_response = response + 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( + logger.info( u"send_messages.sent from='{}' recipients='{}' message_id='{}' request_id='{}' " u"ses-configuration-set='{}'".format( message.from_email, @@ -240,7 +290,7 @@ class SESBackend(BaseEmailBackend): message.extra_headers['X-SES-CONFIGURATION-SET'] )) else: - logger.debug(u"send_messages.sent from='{}' recipients='{}' message_id='{}' request_id='{}'".format( + logger.info(u"send_messages.sent from='{}' recipients='{}' message_id='{}' request_id='{}'".format( message.from_email, ", ".join(message.recipients()), message.extra_headers['message_id'], @@ -255,26 +305,41 @@ class SESBackend(BaseEmailBackend): message.extra_headers[key] = getattr(err, key, None) if not self.fail_silently: raise - + if not_sent_list: + sent_message.update({"Sent":"%s" % not_sent_list}) + + logger.info("new_conn_created: %s" % new_conn_created) if new_conn_created: + logger.info("closing new connection after send") self.close() - - return num_sent + + logger.info("DEBUGING EMAILS --- list_of_response:%s" % (list_of_response)) + logger.info("DEBUGING EMAILS --- return %s, %s" % (num_sent, sent_message)) + + + return num_sent, sent_message def get_rate_limit(self): + logger.info("getting rate limit") if self._access_key_id in cached_rate_limits: + logger.info("returning cached rate limit %s" % cached_rate_limits[self._access_key_id]) return cached_rate_limits[self._access_key_id] + logger.info("creating AWS connection") new_conn_created = self.open() if not self.connection: + logger.info("AWS connection creation failed") raise Exception( "No connection is available to check current SES rate limit.") try: quota_dict = self.connection.get_send_quota() + logger.info("AWS quota dict %s" % quota_dict) 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() \ No newline at end of file + self.close() + + \ No newline at end of file diff --git a/django_aws_ses/models.py b/django_aws_ses/models.py index 8cd4430..7747349 100644 --- a/django_aws_ses/models.py +++ b/django_aws_ses/models.py @@ -1,5 +1,6 @@ import hashlib import logging +import traceback from django.conf import settings from django.db import models @@ -25,9 +26,15 @@ class AwsSesSettings(models.Model): @receiver(post_save, sender=Site) def update_awsses_settings(sender, instance, created, **kwargs): - if created: - AwsSesSettings.objects.create(Site=instance) - instance.AwsSesSettings.save() + try: + if created: + AwsSesSettings.objects.create(site=instance) + instance.awssessettings.save() + except Exception as e: + print("Exception saving site error:%s" % e) + track = traceback.format_exc() + print("Exception saving site track: %s" % (track)) + class AwsSesUserAddon(models.Model): user = models.OneToOneField(User, related_name='aws_ses', on_delete=models.CASCADE) @@ -87,9 +94,12 @@ class BounceRecord(models.Model): status = models.CharField(max_length=255, blank=True, null=True,) action = models.CharField(max_length=255, blank=True, null=True,) feedback_id = models.TextField(max_length=255, blank=True, null=True,) - diagnostic_code = models.CharField(max_length=255, blank=True, null=True,) + diagnostic_code = models.CharField(max_length=2048, blank=True, null=True,) cleared = models.BooleanField(default=False) + class Meta: + indexes = [models.Index(fields=["email"]),] + 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) @@ -104,7 +114,6 @@ class ComplaintRecord(models.Model): 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' @@ -121,6 +130,9 @@ class SendRecord(models.Model): 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,) + + class Meta: + indexes = [models.Index(fields=["destination"]),] def __str__(self): return "source: %s, destination: %s, subject: %s, date: %s" % (self.source, self.destination, self.subject, self.timestamp) @@ -133,4 +145,10 @@ class UnknownRecord(models.Model): def __str__(self): return "eventType: %s, timestamp: %s" % (self.eventType, self.timestamp) - \ No newline at end of file + +class BlackListedDomains(models.Model): + domain = models.CharField(max_length=255, unique=True) + timestamp = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return "%s, blocked: %s" % (self.domain, self.timestamp) \ No newline at end of file diff --git a/django_aws_ses/settings.py b/django_aws_ses/settings.py index 2796756..9c0c8b3 100644 --- a/django_aws_ses/settings.py +++ b/django_aws_ses/settings.py @@ -1,83 +1,106 @@ from django.conf import settings - +from django.contrib.sites.models import Site import logging from .models import ( AwsSesSettings ) + + +__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') + +aws_ses_Settings = None + try: aws_ses_Settings, c = AwsSesSettings.objects.get_or_create(site_id=settings.SITE_ID) except Exception as e: print("AwsSesSettings does not exist: error: %s" % e) -else: - __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) +ACCESS_KEY = aws_ses_Settings.access_key if aws_ses_Settings else None +if ACCESS_KEY is None: + ACCESS_KEY = getattr(settings, 'AWS_SES_ACCESS_KEY_ID',getattr(settings, 'AWS_ACCESS_KEY_ID', 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__))') +SECRET_KEY = aws_ses_Settings.secret_key if aws_ses_Settings else None +if SECRET_KEY is None: + SECRET_KEY = getattr(settings, 'AWS_SES_SECRET_ACCESS_KEY',getattr(settings, 'AWS_SECRET_ACCESS_KEY', None)) - DEFAULT_FROM_EMAIL = getattr(settings, 'DEFAULT_FROM_EMAIL', 'no_reply@%s' % aws_ses_Settings.site.domain) +AWS_SES_REGION_NAME = aws_ses_Settings.region_name if aws_ses_Settings else None +if AWS_SES_REGION_NAME is None: + AWS_SES_REGION_NAME = getattr(settings, 'AWS_SES_REGION_NAME',getattr(settings, 'AWS_DEFAULT_REGION', 'us-east-1')) - HOME_URL = getattr(settings, 'HOME_URL', '') +AWS_SES_REGION_ENDPOINT = aws_ses_Settings.region_endpoint if aws_ses_Settings else None +if AWS_SES_REGION_ENDPOINT is None: + AWS_SES_REGION_ENDPOINT = getattr(settings, 'AWS_SES_REGION_ENDPOINT','email.us-east-1.amazonaws.com') - UNSUBSCRIBE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/unsebscribe.html') - BASE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/base.html') +BASE_DIR = getattr(settings, 'BASE_DIR', None) + +DEFAULT_FROM_EMAIL = "" +try: + site = Site.objects.get_current() + + DEFAULT_FROM_EMAIL = getattr(settings, 'DEFAULT_FROM_EMAIL', 'no_reply@%s' % site.domain) +except Exception as e: + print("Site Doesn't Exist, please configure Django sites") + print("Error is: %s" % e) - ACCESS_KEY = aws_ses_Settings.access_key or getattr(settings, 'AWS_SES_ACCESS_KEY_ID',getattr(settings, 'AWS_ACCESS_KEY_ID', None)) +HOME_URL = getattr(settings, 'HOME_URL', '') + +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__))') + +UNSUBSCRIBE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/unsebscribe.html') +BASE_TEMPLET = getattr(settings, 'UNSUBSCRIBE_TEMPLET', 'django_aws_ses/base.html') + +AWS_SES_REGION_ENDPOINT_URL = getattr(settings, 'AWS_SES_REGION_ENDPOINT_URL','https://' + AWS_SES_REGION_ENDPOINT) - SECRET_KEY = aws_ses_Settings.secret_key or getattr(settings, 'AWS_SES_SECRET_ACCESS_KEY',getattr(settings, 'AWS_SECRET_ACCESS_KEY', None)) +AWS_SES_AUTO_THROTTLE = getattr(settings, 'AWS_SES_AUTO_THROTTLE', 0.5) +AWS_SES_RETURN_PATH = getattr(settings, 'AWS_SES_RETURN_PATH', None) + +# if AWS_SES_RETURN_PATH is None: +# AWS_SES_RETURN_PATH = "CF Doors " - 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_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) - AWS_SES_REGION_ENDPOINT = aws_ses_Settings.region_endpoint or getattr(settings, 'AWS_SES_REGION_ENDPOINT','email.us-east-1.amazonaws.com') +SES_BACKEND_DEBUG = getattr(settings,'SES_BACKEND_DEBUG', False) - 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') +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') diff --git a/django_aws_ses/utils.py b/django_aws_ses/utils.py index 03a32a7..41fe518 100644 --- a/django_aws_ses/utils.py +++ b/django_aws_ses/utils.py @@ -1,5 +1,9 @@ import base64 import logging +import time +import re +import dns.resolver +from telnetlib import Telnet from builtins import str as text from builtins import bytes from io import StringIO @@ -19,7 +23,9 @@ from . import settings from . import signals from .models import ( BounceRecord, - ComplaintRecord + ComplaintRecord, + BlackListedDomains, + SendRecord, ) logger = settings.logger @@ -211,30 +217,40 @@ def verify_bounce_message(msg): @receiver(signals.email_pre_send) def receiver_email_pre_send(sender, message=None, **kwargs): - logger.info("receiver_email_pre_send received signal") + #logger.info("receiver_email_pre_send received signal") + pass def filter_recipiants(recipiant_list): + logger.info("Starting filter_recipiants: %s" % recipiant_list) + + if type(recipiant_list) != type([]): + logger.info("putting emails into a list") + recipiant_list = [recipiant_list] if len(recipiant_list) > 0: - recipiant_list = filter_recipiants_with_unsubscribe(recipiant_list) - + 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) - + + if len(recipiant_list) > 0: + recipiant_list = filter_recipiants_with_validater_email_domain(recipiant_list) + + logger.info("recipiant list after filter_recipiants: %s" % 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("unsubscribe filter running") - logger.info("message.recipients() befor blacklist_emails filter: %s" % recipiant_list) + #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: @@ -244,9 +260,9 @@ 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("complaint_records filter running") - logger.info("message.recipients() befor blacklist_emails filter: %s" % recipiant_list) + #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: @@ -259,21 +275,87 @@ 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("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)])) - 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 + filter message recipiants with a list of emails 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] + 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 +def filter_recipiants_with_validater_email_domain(recipiant_list): + debug_flag = True + + sent_list = [e.destination for e in SendRecord.objects.filter(destination__in=recipiant_list).distinct("destination")] + + test_list = [e for e in recipiant_list if e not in sent_list] + + for e in test_list: + + if not validater_email_domain(e): + recipiant_list.remove(e) + + return recipiant_list + +def validater_email_domain(email): + + if email.find("@") < 1: + + return False + domain = email.split("@")[-1] + + if BlackListedDomains.objects.filter(domain=domain).count() > 0: + return False + + records = [] + try: + records = dns.resolver.query(domain, 'MX') + except dns.resolver.NoNameservers as e: + return False + + except dns.resolver.NoAnswer as e: + return False + + except dns.resolver.NXDOMAIN as e: + return False + + except dns.resolver.LifetimeTimeout as e: + return False + + if len(records) < 1: + return False + + return True + +def emailIsValid(email): + + resp = False + regex = re.compile(r'([A-Za-z0-9]+[.\-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(.[A-Z|a-z]{2,})+') + if re.fullmatch(regex, email): + resp = True + + return resp + +def validate_email(email): + + if not emailIsValid(email): + return False + + if BounceRecord.objects.filter(email=email).count() >= settings.SES_BOUNCE_LIMIT: + return False + + if ComplaintRecord.objects.filter(email=email).count() > 0: + return False + + return validater_email_domain(email) \ No newline at end of file diff --git a/django_aws_ses/views.py b/django_aws_ses/views.py index aa54d1d..dd598e9 100644 --- a/django_aws_ses/views.py +++ b/django_aws_ses/views.py @@ -249,6 +249,13 @@ def handle_bounce(request): 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. logger.info('create records') @@ -265,13 +272,6 @@ def handle_bounce(request): 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,