├── tests ├── __init__.py ├── requirements.txt └── test_backends.py ├── email_hijacker ├── backends │ ├── __init__.py │ └── hijacker.py ├── __init__.py ├── defaults.py └── apps.py ├── setup.cfg ├── requirements.txt ├── MANIFEST.in ├── .travis.yml ├── .gitignore ├── LICENSE ├── tox.ini ├── README.rst └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /email_hijacker/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 -------------------------------------------------------------------------------- /email_hijacker/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.2' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.5 2 | django-pods>=1.0 3 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | tox 2 | coverage 3 | nose 4 | nose-exclude 5 | mock -------------------------------------------------------------------------------- /email_hijacker/defaults.py: -------------------------------------------------------------------------------- 1 | EMAIL_BACKEND = None 2 | EMAIL_ADDRESS = None 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include setup.py 4 | include tox.ini 5 | include requirements.txt 6 | recursive-include email_hijacker * 7 | recursive-exclude email_hijacker *.pyc 8 | recursive-exclude email_hijacker *.pyo 9 | recursive-exclude docs * 10 | recursive-exclude tests * 11 | -------------------------------------------------------------------------------- /email_hijacker/apps.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.apps import AppConfig 3 | except: # pragma: no cover 4 | class AppConfig(object): 5 | def __init__(self, *args, **kwargs): 6 | pass 7 | 8 | from pods.apps import AppSettings 9 | 10 | 11 | class EmailHijacker(AppSettings, AppConfig): 12 | name = "email_hijacker" 13 | settings_module = "email_hijacker.defaults" 14 | settings_key = "HIJACKER" 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | - TOXENV=py27-1.5 5 | - TOXENV=py27-1.6 6 | - TOXENV=py27-1.7 7 | - TOXENV=py27-1.8 8 | - TOXENV=py33-1.5 9 | - TOXENV=py33-1.6 10 | - TOXENV=py33-1.7 11 | - TOXENV=py33-1.8 12 | - TOXENV=py34-1.5 13 | - TOXENV=py34-1.6 14 | - TOXENV=py34-1.7 15 | - TOXENV=py34-1.8 16 | 17 | install: 18 | - pip install tox coveralls 19 | 20 | script: 21 | - tox 22 | 23 | after_success: coveralls 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | cover/ 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | .idea/ 46 | 47 | # Rope 48 | .ropeproject 49 | 50 | # Django stuff: 51 | *.log 52 | *.pot 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # Mr OS 58 | .AppleDouble 59 | .DS_Store 60 | *.swp 61 | *.tmp 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 OohlaLabs Limited 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /email_hijacker/backends/hijacker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.core.mail.backends.base import BaseEmailBackend 6 | 7 | try: 8 | from django.utils.module_loading import import_string 9 | except ImportError: # pragma: nocover 10 | from django.utils.module_loading import import_by_path as import_string 11 | 12 | from ..apps import EmailHijacker 13 | 14 | 15 | class EmailBackend(BaseEmailBackend): 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(EmailBackend, self).__init__(*args, **kwargs) 19 | self.hijacked = import_string(EmailHijacker.EMAIL_BACKEND)(*args, **kwargs) 20 | 21 | def open(self): 22 | self.hijacked.open() 23 | 24 | def close(self): 25 | self.hijacked.close() 26 | 27 | def send_messages(self, email_messages): 28 | for email in email_messages: 29 | email.to = (EmailHijacker.EMAIL_ADDRESS,) 30 | email.bcc = () 31 | email.cc = () 32 | email.subject = 'HIJACKED: {}'.format(email.subject) 33 | self.hijacked.send_messages(email_messages) 34 | -------------------------------------------------------------------------------- /tests/test_backends.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import Mock 3 | from django.core import mail 4 | from django.conf import settings 5 | 6 | 7 | settings.configure( 8 | EMAIL_BACKEND="email_hijacker.backends.hijacker.EmailBackend", 9 | HIJACKER_EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", 10 | HIJACKER_EMAIL_ADDRESS="hijacker@example.org", 11 | ) 12 | 13 | 14 | class TestEmailBackend(unittest.TestCase): 15 | 16 | def test_open(self): 17 | backend = mail.get_connection(settings.EMAIL_BACKEND) 18 | backend.hijacked.open = mock = Mock() 19 | backend.open() 20 | self.assertTrue(mock.called) 21 | self.assertEquals(mock.call_count, 1) 22 | 23 | def test_close(self): 24 | backend = mail.get_connection(settings.EMAIL_BACKEND) 25 | backend.hijacked.close = mock = Mock() 26 | backend.close() 27 | self.assertTrue(mock.called) 28 | self.assertEquals(mock.call_count, 1) 29 | 30 | def test_email_message(self): 31 | mail.EmailMessage("Subject", "Message", from_email=None, to=["to@example.org"], cc=["cc@example.org"], bcc=["bcc@example.org"]).send() 32 | message = mail.outbox.pop() 33 | self.assertEqual("HIJACKED: Subject", message.subject) 34 | self.assertEqual((settings.HIJACKER_EMAIL_ADDRESS,), message.to) 35 | self.assertEqual((), message.cc) 36 | self.assertEqual((), message.bcc) 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27-1.5, py27-1.6, py27-1.7, py27-1.8, 4 | py33-1.5, py33-1.6, py33-1.7, py33-1.8, 5 | py34-1.5, py34-1.6, py34-1.7, py34-1.8 6 | 7 | [testenv] 8 | usedevelop = true 9 | commands = 10 | nosetests --with-coverage --cover-package=email_hijacker 11 | django15 = 12 | Django>=1.5,<1.6 13 | -r{toxinidir}/tests/requirements.txt 14 | django16 = 15 | Django>=1.6,<1.7 16 | -r{toxinidir}/tests/requirements.txt 17 | django17 = 18 | Django>=1.7,<1.8 19 | -r{toxinidir}/tests/requirements.txt 20 | django18 = 21 | Django==1.8b1 22 | -r{toxinidir}/tests/requirements.txt 23 | 24 | [testenv:py27-1.5] 25 | basepython = python2.7 26 | deps = {[testenv]django15} 27 | 28 | [testenv:py27-1.6] 29 | basepython = python2.7 30 | deps = {[testenv]django16} 31 | 32 | [testenv:py27-1.7] 33 | basepython = python2.7 34 | deps = {[testenv]django17} 35 | 36 | [testenv:py27-1.8] 37 | basepython = python2.7 38 | deps = {[testenv]django18} 39 | 40 | [testenv:py33-1.5] 41 | basepython = python3.3 42 | deps = {[testenv]django15} 43 | 44 | [testenv:py33-1.6] 45 | basepython = python3.3 46 | deps = {[testenv]django16} 47 | 48 | [testenv:py33-1.7] 49 | basepython = python3.3 50 | deps = {[testenv]django17} 51 | 52 | [testenv:py33-1.8] 53 | basepython = python3.3 54 | deps = {[testenv]django18} 55 | 56 | [testenv:py34-1.5] 57 | basepython = python3.4 58 | deps = {[testenv]django15} 59 | 60 | [testenv:py34-1.6] 61 | basepython = python3.4 62 | deps = {[testenv]django16} 63 | 64 | [testenv:py34-1.7] 65 | basepython = python3.4 66 | deps = {[testenv]django17} 67 | 68 | [testenv:py34-1.8] 69 | basepython = python3.4 70 | deps = {[testenv]django18} 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Django Email Hijacker 3 | ===================== 4 | 5 | .. image:: https://travis-ci.org/jthi3rry/django-email-hijacker.svg?branch=master 6 | :target: https://travis-ci.org/jthi3rry/django-email-hijacker 7 | 8 | .. image:: https://coveralls.io/repos/jthi3rry/django-email-hijacker/badge.png?branch=master 9 | :target: https://coveralls.io/r/jthi3rry/django-email-hijacker 10 | 11 | Django Email Hijacker lets you send emails via your favourite email backend but sends them to a specified email address instead of the intended recipients. 12 | 13 | It allows you to send emails via a real backend from a development or staging environment without worrying that an actual user might get sent unintended emails. 14 | 15 | 16 | Installation 17 | ------------ 18 | :: 19 | 20 | pip install django-email-hijacker 21 | 22 | 23 | In your development or staging ``settings.py``:: 24 | 25 | 26 | EMAIL_BACKEND = "email_hijacker.backends.hijacker.EmailBackend" 27 | 28 | HIJACKER_EMAIL_ADDRESS = "hijacker@example.org" 29 | HIJACKER_EMAIL_BACKEND = "your.real.EmailBackend" 30 | 31 | 32 | .. note:: Django Email Hijacker uses `Django Pods `_. 33 | 34 | It allows for prefix style settings:: 35 | 36 | 37 | HIJACKER_EMAIL_ADDRESS = "hijacker@example.org" 38 | HIJACKER_EMAIL_BACKEND = "your.real.EmailBackend" 39 | 40 | 41 | Or dictionary style settings:: 42 | 43 | HIJACKER = { 44 | "EMAIL_ADDRESS": "hijacker@example.org", 45 | "EMAIL_BACKEND": "your.real.EmailBackend" 46 | } 47 | 48 | 49 | Running Tests 50 | ------------- 51 | :: 52 | 53 | tox 54 | 55 | 56 | Contributions 57 | ------------- 58 | 59 | All contributions and comments are welcome. 60 | 61 | Change Log 62 | ---------- 63 | 64 | v0.3.2 65 | ~~~~~~ 66 | * Fix #2: exclude tests package 67 | 68 | v0.3.1 69 | ~~~~~~ 70 | * Switch to Semantic Versioning 71 | * Fix issue with parse_requirements for newer versions of pip (>=6.0.0) 72 | 73 | v0.3 74 | ~~~~ 75 | * Use Django Pods for settings 76 | 77 | v0.2 78 | ~~~~ 79 | * Unit tests now use Django 1.7 final instead of RC1 80 | 81 | v0.1 82 | ~~~~ 83 | * Initial 84 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | import uuid 5 | import codecs 6 | from setuptools import setup, find_packages 7 | from setuptools.command.test import test as TestCommand 8 | from pip.req import parse_requirements 9 | 10 | 11 | class Tox(TestCommand): 12 | 13 | def finalize_options(self): 14 | TestCommand.finalize_options(self) 15 | self.test_args = [] 16 | self.test_suite = True 17 | 18 | def run_tests(self): 19 | #import here, cause outside the eggs aren't loaded 20 | import tox 21 | errno = tox.cmdline(self.test_args) 22 | sys.exit(errno) 23 | 24 | 25 | def read(*parts): 26 | filename = os.path.join(os.path.dirname(__file__), *parts) 27 | with codecs.open(filename, encoding='utf-8') as fp: 28 | return fp.read() 29 | 30 | 31 | def find_version(*file_paths): 32 | version_file = read(*file_paths) 33 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) 34 | if version_match: 35 | return version_match.group(1) 36 | raise RuntimeError("Unable to find version string.") 37 | 38 | 39 | pypi_readme_note = """\ 40 | .. note:: 41 | 42 | For the latest source, discussions, etc., please visit the 43 | `GitHub repository `_ 44 | """ 45 | 46 | 47 | setup( 48 | name='django-email-hijacker', 49 | version=find_version('email_hijacker', '__init__.py'), 50 | author='OohlaLabs Limited', 51 | author_email='packages@oohlalabs.co.nz', 52 | maintainer="Thierry Jossermoz", 53 | maintainer_email="thierry.jossermoz@oohlalabs.com", 54 | url='https://github.com/OohlaLabs/django-email-hijacker', 55 | packages=find_packages(), 56 | install_requires=[str(ir.req) for ir in parse_requirements('requirements.txt', session=uuid.uuid1())], 57 | tests_require=['tox'], 58 | cmdclass={'test': Tox}, 59 | license='MIT', 60 | description='Email Hijacker for Django', 61 | long_description="\n\n".join([pypi_readme_note, read('README.rst')]), 62 | classifiers=[ 63 | "Development Status :: 3 - Alpha", 64 | "Environment :: Web Environment", 65 | "Intended Audience :: Developers", 66 | "Operating System :: OS Independent", 67 | "Programming Language :: Python", 68 | "Programming Language :: Python :: 2.7", 69 | "Programming Language :: Python :: 3.3", 70 | "Programming Language :: Python :: 3.4", 71 | 'Topic :: Software Development :: Libraries :: Python Modules', 72 | "Framework :: Django", 73 | ] 74 | ) --------------------------------------------------------------------------------