diff --git a/django_aws_ses/admin.py b/django_aws_ses/admin.py index 01a27d8..f89b316 100644 --- a/django_aws_ses/admin.py +++ b/django_aws_ses/admin.py @@ -11,7 +11,7 @@ from .models import ( from . import settings -logger = settings.logger +#logger = settings.logger class AwsSesSettingsAdmin(admin.ModelAdmin): model = AwsSesSettings @@ -44,7 +44,7 @@ admin.site.register(SESStat, SESStatAdmin) class AdminEmailListFilter(admin.SimpleListFilter): def queryset(self, request, queryset): - logger.info('self.value(): %s' % self.value()) + #logger.info('self.value(): %s' % self.value()) return queryset.filter(email__contains=self.value()) class BounceRecordAdmin(admin.ModelAdmin): diff --git a/django_aws_ses/backends.py b/django_aws_ses/backends.py index 8f66b23..f7f94ef 100644 --- a/django_aws_ses/backends.py +++ b/django_aws_ses/backends.py @@ -8,6 +8,7 @@ from django.dispatch import Signal from datetime import datetime, timedelta from time import sleep +import sys from . import settings from . import signals @@ -91,6 +92,23 @@ class SESBackend(BaseEmailBackend): """Sends one or more EmailMessage objects and returns the number of email messages sent. """ + + + calling_func = '' + try: + fcount = 0 + while sys._getframe(fcount).f_code.co_name in ['send_messages', 'send', 'mail_admins', 'send_mail', 'emit', 'handle']: + fcount +=1 + + 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("called from %s" , (calling_func)) + + + logger.info("send_messages") if not email_messages: return @@ -188,6 +206,15 @@ class SESBackend(BaseEmailBackend): try: 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.recipients() = %s" % message.recipients()) + + logger.info("dkim_key = %s" % self.dkim_key) + logger.info("dkim_domain = %s" % self.dkim_domain) + logger.info("dkim_selector = %s" % self.dkim_selector) + logger.info("dkim_headers = %s" % str(self.dkim_headers)) response = self.connection.send_raw_email( Source=source or message.from_email, Destinations=message.recipients(), diff --git a/django_aws_ses/models.py b/django_aws_ses/models.py index b4300c8..8cd4430 100644 --- a/django_aws_ses/models.py +++ b/django_aws_ses/models.py @@ -60,7 +60,7 @@ def update_awsses_user(sender, instance, created, **kwargs): if created: AwsSesUserAddon.objects.create(user=instance) try: - instance.AwsSesUserAddon.save() + instance.aws_ses.save() except AwsSesUserAddon.DoesNotExist: AwsSesUserAddon.objects.create(user=instance) @@ -86,7 +86,7 @@ class BounceRecord(models.Model): 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,) + feedback_id = models.TextField(max_length=255, blank=True, null=True,) diagnostic_code = models.CharField(max_length=255, blank=True, null=True,) cleared = models.BooleanField(default=False) @@ -97,7 +97,7 @@ 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_id = models.TextField(max_length=255, blank=True, null=True,) feedback_type = models.CharField(max_length=255, blank=True, null=True,) def __str__(self): @@ -116,8 +116,8 @@ class SendRecord(models.Model): 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,) + subject = models.TextField(max_length=255, blank=True, null=True,) + message_id = models.TextField(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,) diff --git a/django_aws_ses/settings.py b/django_aws_ses/settings.py index 3886dfc..2796756 100644 --- a/django_aws_ses/settings.py +++ b/django_aws_ses/settings.py @@ -5,78 +5,79 @@ 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) +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') -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') + 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') diff --git a/django_aws_ses/urls.py b/django_aws_ses/urls.py index 4ef9ef7..f2940fc 100644 --- a/django_aws_ses/urls.py +++ b/django_aws_ses/urls.py @@ -1,5 +1,4 @@ from django.conf.urls import include, url -from django.urls import path from django.views.decorators.csrf import csrf_exempt from .views import ( diff --git a/django_aws_ses/utils.py b/django_aws_ses/utils.py index 36b8d5e..03a32a7 100644 --- a/django_aws_ses/utils.py +++ b/django_aws_ses/utils.py @@ -17,7 +17,6 @@ User = get_user_model() from . import settings from . import signals -from . import utils from .models import ( BounceRecord, ComplaintRecord diff --git a/django_aws_ses/views.py b/django_aws_ses/views.py index 5881fb7..aa54d1d 100644 --- a/django_aws_ses/views.py +++ b/django_aws_ses/views.py @@ -365,8 +365,8 @@ def handle_bounce(request): 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, + u'Received delivery notification: messageId: %s', + message_id, extra={ 'notification': notification, }, @@ -375,7 +375,7 @@ def handle_bounce(request): signals.delivery_received.send( sender=handle_bounce, mail_obj=mail_obj, - delivery_obj=delivery_obj, + delivery_obj=send_obj, raw_message=raw_json, ) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2f17748 --- /dev/null +++ b/setup.py @@ -0,0 +1,147 @@ +import ast +import os +import re +import sys + +from fnmatch import fnmatchcase + +from distutils.util import convert_path +from setuptools import setup, find_packages + + +def read(*path): + return open(os.path.join(os.path.abspath(os.path.dirname(__file__)), + *path)).read() + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ["*.py", "*.pyc", "*~", ".*", "*.bak"] +standard_exclude_directories = [ + ".*", "CVS", "_darcs", "./build", + "./dist", "EGG-INFO", "*.egg-info" +] + +# Copied from paste/util/finddata.py +def find_package_data(where=".", package="", exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + The dictionary looks like:: + {"package": [files]} + Where ``files`` is a list of all the files in that package that + don't match anything in ``exclude``. + If ``only_in_packages`` is true, then top-level directories that + are not packages won't be included (but directories under packages + will). + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + If ``show_ignored`` is true, then all the files that aren't + included in package data are shown on stderr (for debugging + purposes). + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + + out = {} + stack = [(convert_path(where), "", package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) or + fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + sys.stderr.write("Directory %s ignored by pattern %s" % (fn, pattern)) + break + if bad_name: + continue + if os.path.isfile(os.path.join(fn, "__init__.py")) \ + and not prefix: + if not package: + new_package = name + else: + new_package = package + "." + name + stack.append((fn, "", new_package, False)) + else: + stack.append((fn, prefix + name + "/", package, + only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) or + fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + sys.stderr.write("File %s ignored by pattern %s" % (fn, pattern)) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix + name) + return out + +excluded_directories = standard_exclude_directories + ["example", "tests"] +package_data = find_package_data(exclude_directories=excluded_directories) + +DESCRIPTION = "A Django email backend for Amazon's Simple Email Service" + +LONG_DESCRIPTION = None +try: + LONG_DESCRIPTION = open('README.rst').read() +except Exception: + pass + +# Parse version +_version_re = re.compile(r"VERSION\s+=\s+(.*)") +with open("django_aws_ses/__init__.py", "rb") as f: + version = ".".join( + map(str, ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))) + ) + +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Framework :: Django', + 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', +] + +setup( + name='django_aws_ses', + version=version, + packages=find_packages(exclude=['example', 'tests']), + package_data=package_data, + python_requires='>=2.7', + author='ZeeksGeeks', + author_email='development@zeeksgeeks.com', + url='https://github.com/django-ses/django-ses', + license='MIT', + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + platforms=['any'], + classifiers=CLASSIFIERS, + install_requires=["boto3>=1.0.0", "pytz>=2016.10", "future>=0.16.0", "django>1.10"], + include_package_data=True, + extras_require={ + 'bounce': ['requests<3', 'M2Crypto'], + }, +) \ No newline at end of file