diff --git a/.gitignore b/.gitignore index 4cbcf06..055cd6c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,38 @@ -# environment directory -.venv/ -.pydevproject +# Python +*.pyc +__pycache__/ +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ env/ -/.project -/logs -/media -/static -__pycache__ -/*/migrations +.venv/ +*.egg + +# Django +*.log +*.pot +*.sqlite3 +media/ +static/ +migrations/ *.code-workspace -.env/ + +# IDE/Editor .vscode/ -memory-bank/ -.cursor/rules/ -run/celery/ +.pydevproject +.project +.idea/ +*.sublime-project +*.sublime-workspace + +# OS/Environment +.DS_Store +.env +*.bak +*~ + +# Package-specific +aws_ses.log \ No newline at end of file diff --git a/LICENSE b/LICENSE index f60de74..8aaac87 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,21 @@ -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. \ No newline at end of file +MIT License + +Copyright (c) 2025 ZeeksGeeks + +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. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4d6a5d5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.md +recursive-include django_aws_ses/templates *.html +recursive-include django_aws_ses/static *.css *.js \ No newline at end of file diff --git a/README.md b/README.md index e69de29..9a26496 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,71 @@ +# Django AWS SES + +A Django email backend for sending emails via Amazon Simple Email Service (SES). + +## Features +- Send emails using AWS SES with DKIM signing support. +- Handle bounce, complaint, and delivery notifications via SNS webhooks. +- Filter recipients based on bounce/complaint history and domain validation. +- Admin dashboard for SES statistics and verified emails. +- Unsubscribe functionality with secure URL generation. + +## Installation +```bash +pip install django_aws_ses +``` + +## Requirements +- Python 3.8+ +- Django 3.2+ +- AWS SES account with verified domains/emails + +## Setup +1. Add to `INSTALLED_APPS`: + ```python + INSTALLED_APPS = [ + ... + 'django_aws_ses', + ] + ``` + +2. Configure settings in `settings.py`: + ```python + AWS_SES_ACCESS_KEY_ID = 'your-access-key' + AWS_SES_SECRET_ACCESS_KEY = 'your-secret-key' + AWS_SES_REGION_NAME = 'us-east-1' + AWS_SES_REGION_ENDPOINT = 'email.us-east-1.amazonaws.com' + EMAIL_BACKEND = 'django_aws_ses.backends.SESBackend' + ``` + +3. Apply migrations: + ```bash + python manage.py migrate + ``` + +4. (Optional) Enable DKIM signing: + ```python + DKIM_DOMAIN = 'example.com' + DKIM_PRIVATE_KEY = 'your-private-key' + DKIM_SELECTOR = 'ses' + ``` + +5. Set up SNS webhook for bounce/complaint handling: + - Add the URL `your-domain.com/aws_ses/bounce/` to your SNS subscription. + - Ensure the view is accessible (e.g., CSRF-exempt). + +## Usage +- Send emails using Django’s `send_mail` or `EmailMessage`. +- View SES statistics at `/aws_ses/status/` (superuser only). +- Unsubscribe users via `/aws_ses/unsubscribe///`. + +## Development +To contribute: +1. Clone the repo: `git clone https://github.com/zeeksgeeks/django_aws_ses` +2. Install dependencies: `pip install -r requirements.txt` +3. Run tests: `python manage.py test` + +## License +MIT License. See [LICENSE](LICENSE) for details. + +## Credits +Developed by Ray Jessop. Inspired by [django-ses](https://github.com/django-ses/django-ses). \ No newline at end of file diff --git a/django_aws_ses/__init__.py b/django_aws_ses/__init__.py index 28ad96f..4eef8d1 100644 --- a/django_aws_ses/__init__.py +++ b/django_aws_ses/__init__.py @@ -1,8 +1,8 @@ +"""Django AWS SES: A Django email backend for Amazon SES.""" 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]) +VERSION = (0, 1, 0) +__version__ = '.'.join(str(x) for x in VERSION) __author__ = 'Ray Jessop' -__all__ = ('Django AWS SES Backend',) +__all__ = ('SESBackend', 'DjangoAwsSesBackendConfig') \ No newline at end of file diff --git a/setup.py b/setup.py index 2f17748..bedad5e 100644 --- a/setup.py +++ b/setup.py @@ -1,147 +1,45 @@ -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', -] +with open("README.md", "r", encoding="utf-8") as f: + long_description = f.read() 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"], + name="django_aws_ses", + version="0.1.0", + packages=find_packages(exclude=["tests"]), include_package_data=True, + python_requires=">=3.8", + install_requires=[ + "django>=3.2", + "boto3>=1.18", + "requests>=2.25", + "cryptography>=3.4", + "dnspython>=2.1", + "pytz>=2021.1", + ], extras_require={ - 'bounce': ['requests<3', 'M2Crypto'], + "dkim": ["dkimpy>=1.0"], }, + author="Ray Jessop", + author_email="development@zeeksgeeks.com", + description="A Django email backend for Amazon SES", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/zeeksgeeks/django_aws_ses", + license="MIT", + classifiers=[ + "Development Status :: 4 - Beta", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Communications :: Email", + ], ) \ No newline at end of file