diff --git a/.gitignore b/.gitignore index 2f78cf5b..b165bdab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.pyc - +_* +/django_mailer_2.egg-info/dependency_links.txt +/django_mailer_2.egg-info/PKG-INFO +/django_mailer_2.egg-info/SOURCES.txt +/django_mailer_2.egg-info/top_level.txt diff --git a/MANIFEST.in b/MANIFEST.in index f68c3674..645fcc9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE recursive-include docs * +recursive-include django_mailer/templates * diff --git a/README b/README index cc1f01b5..719d4b10 100644 --- a/README +++ b/README @@ -1,3 +1,13 @@ -django-mailer-2 by Chris Beaven (a fork of James Tauber's django-mailer) +WARNING: This fork is no longer maintained, use https://github.com/APSL/django-yubin. -A reusable Django app for queuing the sending of email \ No newline at end of file +django-mailer-2 by APSL is a Chris Beaven form from a fork of +James Tauber's django-mailer. + +A reusable Django app for queuing the sending of email. + +Execute runtests.py to run the test. You'll need a working mail server +or just + +python -m smtpd -n -c DebuggingServer localhost:1025 + +Find documentation about configuration an use on: https://django-mailer2.readthedocs.org/en/latest/ diff --git a/bin/cleanup_mail b/bin/cleanup_mail new file mode 100755 index 00000000..d80ab833 --- /dev/null +++ b/bin/cleanup_mail @@ -0,0 +1,13 @@ +#!/bin/bash +# cron job example to run cleanup_mail and delete old mails +# replace {{username}} with the user who is going to execute the task +# the sample assumes you have virtualenv and virtualenvwrapper installed +# and that the applicacion is installed in src + +USER={{username}} +export WORKON_HOME=/$HOME/.virtualenvs +source /usr/local/bin/virtualenvwrapper.sh +VIRTUALENV=$USER +workon $VIRTUALENV +cd $HOME/src +python manage.py cleanup_mail --days=90 diff --git a/bin/fake-server b/bin/fake-server new file mode 100755 index 00000000..35649e97 --- /dev/null +++ b/bin/fake-server @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Runing a fake smptp server on localhost:1020" + +python -m smtpd -n -c DebuggingServer localhost:1025 diff --git a/bin/retry_deferred b/bin/retry_deferred new file mode 100755 index 00000000..52e6f1d8 --- /dev/null +++ b/bin/retry_deferred @@ -0,0 +1,13 @@ +#!/bin/bash +# cron job example to run retry_deferred +# replace {{username}} with the user who is going to execute the task +# the sample assumes you have virtualenv and virtualenvwrapper installed +# and that the applicacion is installed in src + +USER={{username}} +export WORKON_HOME=/$HOME/.virtualenvs +source /usr/local/bin/virtualenvwrapper.sh +VIRTUALENV=$USER +workon $VIRTUALENV +cd $HOME/src +python manage.py retry_deferred diff --git a/bin/send_mails b/bin/send_mails new file mode 100755 index 00000000..4a59326e --- /dev/null +++ b/bin/send_mails @@ -0,0 +1,13 @@ +#!/bin/bash +# replace {{username}} with the user who is going to execute the task +# the sample assumes you have virtualenv and virtualenvwrapper installed +# and that the applicacion is installed in src + +USER={{usern{{username}} +export WORKON_HOME=/$HOME/.virtualenvs +source /usr/local/bin/virtualenvwrapper.sh +VIRTUALENV=$USER + +workon $VIRTUALENV +cd $HOME/src +python manage.py send_mail diff --git a/demo/demo.sqlite b/demo/demo.sqlite new file mode 100644 index 00000000..4fe660fd Binary files /dev/null and b/demo/demo.sqlite differ diff --git a/demo/demo/__init__.py b/demo/demo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/demo/demo/management/__init__.py b/demo/demo/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/demo/demo/management/commands/__init__.py b/demo/demo/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/demo/demo/management/commands/testmail.py b/demo/demo/management/commands/testmail.py new file mode 100644 index 00000000..3f5dd919 --- /dev/null +++ b/demo/demo/management/commands/testmail.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + + +from django.core.management.base import BaseCommand, CommandError +from optparse import make_option +from django.core.mail import send_mail + +class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option('--quantity', + dest='qtt', + default=False, + help = 'Generates a number of fake mails for testing' + ), + ) + + def handle(self, *args, **options): + number=int(options['qtt']) + for i in range(0, number): + send_mail('test %s' %i , 'body %s' % i, + 'test@example.com', ['recipient%s@example.com' %i, ]) + self.stdout.write('Generated') + diff --git a/demo/demo/settings.py b/demo/demo/settings.py new file mode 100644 index 00000000..fdf18cc3 --- /dev/null +++ b/demo/demo/settings.py @@ -0,0 +1,154 @@ +# Django settings for demo project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'demo.sqlite', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '&-8hi+rl5oi(_kenr3^q3+tw#rayvmzuiu*i70)ase+tok!(o7' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'demo.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'demo.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + 'django_mailer', + 'demo', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} +EMAIL_BACKEND = 'django_mailer.smtp_queue.EmailBackend' diff --git a/demo/demo/templates/base.html b/demo/demo/templates/base.html new file mode 100644 index 00000000..131ae2fa --- /dev/null +++ b/demo/demo/templates/base.html @@ -0,0 +1,58 @@ +{% load static i18n %}{% load url from future %} + + + + + + + {% block head_title_base %}{% block head_title %}{% endblock %} | {% trans "Project Name" %}{% endblock %} + + + + {% block style_base %} + {% block extra_style %}{% endblock %} + {% endblock %} + + {% block extra_head_base %} + {% block extra_head %}{% endblock %} + {% block analytics %} + {% endblock analytics %} + {% endblock %} + + +
+ {% block topbar_base %} + {% endblock %} + +
+ {% block ieprompt %}{% endblock %} + + + {% block page_header_base %} + {% block page_header %}{% endblock %} + {% endblock %} + +
+ {% block content %}{% endblock %} +
+
+ +
{# sticky footer push #} +
{# /wrap #} + + {% block footer_base %} + {% endblock %} + + {% block script_base %} + {% block extra_script %}{% endblock %} + {% endblock %} + + {% block extra_body_base %} + {% block extra_body %}{% endblock %} + {% endblock %} + + {% block modal_base %}{% endblock %} + + diff --git a/demo/demo/templates/index.html b/demo/demo/templates/index.html new file mode 100644 index 00000000..94d9808c --- /dev/null +++ b/demo/demo/templates/index.html @@ -0,0 +1 @@ +{% extends "base.html" %} diff --git a/demo/demo/urls.py b/demo/demo/urls.py new file mode 100644 index 00000000..de022736 --- /dev/null +++ b/demo/demo/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +from demo.views import IndexView + +urlpatterns = patterns('', + # Examples: + url(r'^$', IndexView.as_view(), name='home'), + url(r'^admin/', include(admin.site.urls)), +) diff --git a/demo/demo/views.py b/demo/demo/views.py new file mode 100644 index 00000000..72dfa014 --- /dev/null +++ b/demo/demo/views.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +from django.views.generic import TemplateView + +class IndexView(TemplateView): + template_name = 'index.html' diff --git a/demo/demo/wsgi.py b/demo/demo/wsgi.py new file mode 100644 index 00000000..28f36bb4 --- /dev/null +++ b/demo/demo/wsgi.py @@ -0,0 +1,28 @@ +""" +WSGI config for demo project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/demo/manage.py b/demo/manage.py new file mode 100755 index 00000000..86cc0b09 --- /dev/null +++ b/demo/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/demo/requirements.txt b/demo/requirements.txt new file mode 100644 index 00000000..5899fdad --- /dev/null +++ b/demo/requirements.txt @@ -0,0 +1,3 @@ +django +pyzmail +#and django_mailer2 of course diff --git a/django_mailer/__init__.py b/django_mailer/__init__.py index be0b1c22..79cb4409 100644 --- a/django_mailer/__init__.py +++ b/django_mailer/__init__.py @@ -1,9 +1,12 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + import logging -VERSION = (1, 2, 0) +VERSION = (1, 3, 2) logger = logging.getLogger('django_mailer') -logger.setLevel(logging.DEBUG) def get_version(): @@ -49,7 +52,7 @@ def mail_admins(subject, message, fail_silently=False, priority=None): """ from django.conf import settings as django_settings from django.utils.encoding import force_unicode - from django_mailer import constants, settings + from django_mailer import settings if priority is None: settings.MAIL_ADMINS_PRIORITY diff --git a/django_mailer/admin.py b/django_mailer/admin.py index ecbbe552..3a5309fe 100644 --- a/django_mailer/admin.py +++ b/django_mailer/admin.py @@ -1,14 +1,84 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + from django.contrib import admin from django_mailer import models +try: + from django.conf.urls import patterns, url +except ImportError: + from django.conf.urls.defaults import * +from mail_utils import get_attachments, get_attachment +from pyzmail.parse import message_from_string +from django.shortcuts import render +from django.http import HttpResponse +from django.core.urlresolvers import reverse class Message(admin.ModelAdmin): - list_display = ('to_address', 'subject', 'date_created') + def message_link(self, obj): + url = reverse('admin:mail_detail', args=(obj.id,)) + return """show""" % url + message_link.allow_tags = True + message_link.short_description = u'Show' + + list_display = ('from_address', 'to_address', 'subject', 'date_created', 'message_link') list_filter = ('date_created',) - search_fields = ('to_address', 'subject', 'from_address', 'encoded_message',) + search_fields = ('to_address', 'subject', 'from_address', + 'encoded_message',) date_hierarchy = 'date_created' ordering = ('-date_created',) + def get_urls(self): + urls = super(Message, self).get_urls() + custom_urls = patterns('', + url(r'^mail/(?P\d+)/$', + self.admin_site.admin_view(self.detail_view), + name='mail_detail'), + url('^mail/attachment/(?P\d+)/(?P[0-9a-f]{32})/$', + self.admin_site.admin_view(self.download_view), + name="mail_download"), + url('^mail/html/(?P\d+)/$', + self.admin_site.admin_view(self.html_view), + name="mail_html")) + return custom_urls + urls + + def detail_view(self, request, pk): + instance = models.Message.objects.get(pk=pk) + payload_str = instance.encoded_message.encode('utf-8') + msg = message_from_string(payload_str) + context = {} + context['subject'] = msg.get_subject() + context['from'] = msg.get_address('from') + context['to'] = msg.get_addresses('to') + context['cc'] = msg.get_addresses('cc') + msg_text = msg.text_part.get_payload() if msg.text_part else None + msg_html = msg.html_part.get_payload() if msg.html_part else None + context['msg_html'] = msg_html + context['msg_text'] = msg_text + context['attachments'] = get_attachments(msg) + context['is_popup'] = True + context['object'] = instance + return render(request, 'django_mailer/message_detail.html', context) + + def download_view(self, request, pk, firma): + payload_str = models.Message.objects.get( + pk=pk).encoded_message.encode('utf-8') + msg = message_from_string(payload_str) + arx = get_attachment(msg, key=firma) + response = HttpResponse(mimetype=arx.tipus) + response['Content-Disposition'] = 'filename=' + arx.filename + response.write(arx.payload) + return response + + def html_view(self, request, pk): + msg = models.Message.objects.get(pk=pk) + payload_str = msg.encoded_message.encode('utf-8') + msg = message_from_string(payload_str) + msg_html = msg.html_part.get_payload() if msg.html_part else None + context = {} + context['msg_html'] = msg_html + return render(request, 'django_mailer/html_detail.html', context) class MessageRelatedModelAdmin(admin.ModelAdmin): list_select_related = True @@ -17,6 +87,10 @@ def message__to_address(self, obj): return obj.message.to_address message__to_address.admin_order_field = 'message__to_address' + def message__from_address(self, obj): + return obj.message.from_address + message__from_address.admin_order_field = 'message__from_address' + def message__subject(self, obj): return obj.message.subject message__subject.admin_order_field = 'message__subject' @@ -32,8 +106,15 @@ def not_deferred(self, obj): not_deferred.boolean = True not_deferred.admin_order_field = 'deferred' - list_display = ('id', 'message__to_address', 'message__subject', - 'message__date_created', 'priority', 'not_deferred') + def message_link(self, obj): + url = reverse('admin:mail_detail', args=(obj.message.id,)) + return """%s""" % (url, obj.message) + message_link.allow_tags = True + message_link.short_description = u'Message' + + list_display = ('id', 'message_link', 'message__to_address', + 'message__from_address', 'message__subject', + 'message__date_created', 'priority', 'not_deferred') class Blacklist(admin.ModelAdmin): @@ -41,8 +122,14 @@ class Blacklist(admin.ModelAdmin): class Log(MessageRelatedModelAdmin): + def message_link(self, obj): + url = reverse('admin:mail_detail', args=(obj.message.id,)) + return """show""" % url + message_link.allow_tags = True + message_link.short_description = u'Message' + list_display = ('id', 'result', 'message__to_address', 'message__subject', - 'date') + 'date', 'message_link') list_filter = ('result',) list_display_links = ('id', 'result') diff --git a/django_mailer/constants.py b/django_mailer/constants.py index 74b8394e..c5cdfb1c 100644 --- a/django_mailer/constants.py +++ b/django_mailer/constants.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + PRIORITY_EMAIL_NOW = 0 PRIORITY_HIGH = 1 PRIORITY_NORMAL = 3 diff --git a/django_mailer/engine.py b/django_mailer/engine.py index cbca94aa..063c8432 100644 --- a/django_mailer/engine.py +++ b/django_mailer/engine.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + """ The "engine room" of django mailer. @@ -181,7 +185,8 @@ def send_queued_message(queued_message, smtp_connection=None, blacklist=None, result = constants.RESULT_SENT except (SocketError, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, - smtplib.SMTPAuthenticationError), err: + smtplib.SMTPAuthenticationError, + UnicodeEncodeError), err: queued_message.defer() logger.warning("Message to %s deferred due to failure: %s" % (message.to_address.encode("utf-8"), err)) @@ -208,7 +213,7 @@ def send_message(email_message, smtp_connection=None): connection can be provided. Otherwise an SMTP connection will be opened to send this message. - This function does not perform any logging or queueing. + This function does not perform any queueing. """ if smtp_connection is None: @@ -223,9 +228,11 @@ def send_message(email_message, smtp_connection=None): result = constants.RESULT_SENT except (SocketError, smtplib.SMTPSenderRefused, smtplib.SMTPRecipientsRefused, - smtplib.SMTPAuthenticationError): + smtplib.SMTPAuthenticationError, + UnicodeEncodeError), err: result = constants.RESULT_FAILED - + logger.warning("Message from %s failed due to: %s" % + (email_message.from_email, err)) if opened_connection: smtp_connection.close() return result diff --git a/django_mailer/lockfile.py b/django_mailer/lockfile.py index bca686cf..80af2227 100644 --- a/django_mailer/lockfile.py +++ b/django_mailer/lockfile.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- """ lockfile.py - Platform-independent advisory file locks. @@ -69,6 +72,7 @@ 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock', 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock'] + class Error(Exception): """ Base class for other exceptions. @@ -80,6 +84,7 @@ class Error(Exception): """ pass + class LockError(Error): """ Base class for error arising from attempts to acquire the lock. @@ -91,6 +96,7 @@ class LockError(Error): """ pass + class LockTimeout(LockError): """Raised when lock creation fails within a user-defined period of time. @@ -101,6 +107,7 @@ class LockTimeout(LockError): """ pass + class AlreadyLocked(LockError): """Some other thread/process is locking the file. @@ -111,6 +118,7 @@ class AlreadyLocked(LockError): """ pass + class LockFailed(LockError): """Lock file creation failed for some other reason. @@ -121,6 +129,7 @@ class LockFailed(LockError): """ pass + class UnlockError(Error): """ Base class for errors arising from attempts to release the lock. @@ -132,6 +141,7 @@ class UnlockError(Error): """ pass + class NotLocked(UnlockError): """Raised when an attempt is made to unlock an unlocked file. @@ -142,6 +152,7 @@ class NotLocked(UnlockError): """ pass + class NotMyLock(UnlockError): """Raised when an attempt is made to unlock a file someone else locked. @@ -152,6 +163,7 @@ class NotMyLock(UnlockError): """ pass + class LockBase: """Base class for platform-specific lock classes.""" def __init__(self, path, threaded=True): @@ -229,6 +241,7 @@ def __exit__(self, *_exc): """ self.release() + class LinkFileLock(LockBase): """Lock access to a file using atomic property of link(2).""" @@ -286,6 +299,7 @@ def break_lock(self): if os.path.exists(self.lock_file): os.unlink(self.lock_file) + class MkdirFileLock(LockBase): """Lock file by creating a directory.""" def __init__(self, path, threaded=True): @@ -360,6 +374,7 @@ def break_lock(self): os.unlink(os.path.join(self.lock_file, name)) os.rmdir(self.lock_file) + class SQLiteFileLock(LockBase): "Demonstration of using same SQL-based locking." @@ -376,7 +391,7 @@ def __init__(self, path, threaded=True): import sqlite3 self.connection = sqlite3.connect(SQLiteFileLock.testdb) - + c = self.connection.cursor() try: c.execute("create table locks" @@ -438,7 +453,7 @@ def acquire(self, timeout=None): if len(rows) == 1: # We're the locker, so go home. return - + # Maybe we should wait a bit longer. if timeout is not None and time.time() > end_time: if timeout > 0: @@ -468,7 +483,7 @@ def _who_is_locking(self): " where lock_file = ?", (self.lock_file,)) return cursor.fetchone()[0] - + def is_locked(self): cursor = self.connection.cursor() cursor.execute("select * from locks" diff --git a/django_mailer/mail_utils.py b/django_mailer/mail_utils.py new file mode 100644 index 00000000..cca9892f --- /dev/null +++ b/django_mailer/mail_utils.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +import hashlib + + +class Attachment(object): + """ + Utility class to contain the attachment information + """ + + def __init__(self, mailpart): + self.filename = mailpart.sanitized_filename + self.tipus = mailpart.type + self.charset = mailpart.charset + self.content_description = mailpart.part.get('Content-Description') + self.payload = mailpart.get_payload() + self.length = len(self.payload) + self.firma = hashlib.md5(self.payload).hexdigest() + + +def get_attachments(msg): + """ + Returns a list with all the mail attachments + """ + + attachments = [] + for mailpart in msg.mailparts: + if not mailpart.is_body and mailpart.disposition == 'attachment': + attachment = Attachment(mailpart) + attachments.append(attachment) + return attachments + + +def get_attachment(msg, key): + """ + Given a msg returns the attachment who's signature (md5 key) matches + the key value + """ + + for mailpart in msg.mailparts: + if not mailpart.is_body and mailpart.disposition == 'attachment': + attachment = Attachment(mailpart) + if attachment.firma == key: + return attachment + return None diff --git a/django_mailer/management/commands/__init__.py b/django_mailer/management/commands/__init__.py index 91440c71..0e27c87f 100644 --- a/django_mailer/management/commands/__init__.py +++ b/django_mailer/management/commands/__init__.py @@ -10,7 +10,7 @@ def create_handler(verbosity, message='%(message)s'): level output depends on the verbosity level). """ handler = logging.StreamHandler() - handler.setLevel(LOGGING_LEVEL[verbosity]) + handler.setLevel(LOGGING_LEVEL[str(verbosity)]) formatter = logging.Formatter(message) handler.setFormatter(formatter) return handler diff --git a/django_mailer/management/commands/cleanup_mail.py b/django_mailer/management/commands/cleanup_mail.py new file mode 100644 index 00000000..84554ee7 --- /dev/null +++ b/django_mailer/management/commands/cleanup_mail.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +import datetime +import logging +from optparse import make_option + +from django.core.management.base import BaseCommand + +from django_mailer.management.commands import create_handler +from django_mailer.models import Message + + +class Command(BaseCommand): + help = 'Delete the mails created before -d days (default 90)' + option_list = BaseCommand.option_list + ( + make_option('-d', '--days', type='int', default=90, + help="Cleanup mails older than this many days, defaults to 90."), + ) + + def handle(self, verbosity, days, **options): + # Delete mails and their related logs and queued created before X days + logger = logging.getLogger('django_mailer') + handler = create_handler(verbosity) + logger.addHandler(handler) + + today = datetime.date.today() + cutoff_date = today - datetime.timedelta(days) + count = Message.objects.filter(date_created__lt=cutoff_date).count() + Message.objects.filter(date_created__lt=cutoff_date).delete() + logger.warning("Deleted %s mails created before %s " % + (count, cutoff_date)) diff --git a/django_mailer/management/commands/retry_deferred.py b/django_mailer/management/commands/retry_deferred.py index a4820650..0692a91a 100644 --- a/django_mailer/management/commands/retry_deferred.py +++ b/django_mailer/management/commands/retry_deferred.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + from django.core.management.base import NoArgsCommand from django_mailer import models from django_mailer.management.commands import create_handler diff --git a/django_mailer/management/commands/send_mail.py b/django_mailer/management/commands/send_mail.py index d609360a..0faebb6d 100644 --- a/django_mailer/management/commands/send_mail.py +++ b/django_mailer/management/commands/send_mail.py @@ -30,7 +30,7 @@ def handle_noargs(self, verbosity, block_size, count, **options): # If this is just a count request the just calculate, report and exit. if count: queued = models.QueuedMessage.objects.non_deferred().count() - deferred = models.QueuedMessage.objects.non_deferred().count() + deferred = models.QueuedMessage.objects.deferred().count() sys.stdout.write('%s queued message%s (and %s deferred message%s).' '\n' % (queued, queued != 1 and 's' or '', deferred, deferred != 1 and 's' or '')) diff --git a/django_mailer/management/commands/status_mail.py b/django_mailer/management/commands/status_mail.py new file mode 100644 index 00000000..799c7ee8 --- /dev/null +++ b/django_mailer/management/commands/status_mail.py @@ -0,0 +1,46 @@ +""" +This command returns the status of the queue in the format: + queued/deferred/seconds + +where: + queued is the number of queued messages we have actually + deferred number of deferred messages we have actually + seconds age in seconds of the oldest messages + +Example: + + 2/0/10 + +means we have 2 queued messages, 0 defered messaged and than the oldest message +in the queue is just 2 seconds old. +""" + +from django.core.management.base import NoArgsCommand +from django_mailer.models import QueuedMessage +import sys +try: + from django.utils.timezone import now +except ImportError: + import datetime + now = datetime.datetime.now + +try: + from django.core.mail import get_connection + EMAIL_BACKEND_SUPPORT = True +except ImportError: + # Django version < 1.2 + EMAIL_BACKEND_SUPPORT = False + + +class Command(NoArgsCommand): + help = "Returns a strig with the queue status as queued/deferred/seconds" + + def handle_noargs(self, *args, **kwargs): + # If this is just a count request the just calculate, report and exit. + queued = QueuedMessage.objects.non_deferred().count() + deferred = QueuedMessage.objects.deferred().count() + oldest = QueuedMessage.objects.non_deferred().order_by('date_queued')[0] + queue_time = now() - oldest.date_queued.replace(tzinfo=None) + seconds = (now() - oldest.date_queued.replace(tzinfo=None)).seconds + sys.stdout.write('%s/%s/%s\n"' % (queued, deferred, seconds)) + sys.exit() diff --git a/django_mailer/managers.py b/django_mailer/managers.py index c8a6ead9..93f6dda7 100644 --- a/django_mailer/managers.py +++ b/django_mailer/managers.py @@ -1,4 +1,13 @@ -import datetime +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +try: + from django.utils.timezone import now +except ImportError: + import datetime + now = datetime.datetime.now + from django.db import models from django_mailer import constants @@ -14,7 +23,7 @@ def exclude_future(self): Exclude future time-delayed messages. """ - return self.exclude(date_queued__gt=datetime.datetime.now) + return self.exclude(date_queued__gt=now) def high_priority(self): """ diff --git a/django_mailer/models.py b/django_mailer/models.py index b50897db..e2a36e72 100644 --- a/django_mailer/models.py +++ b/django_mailer/models.py @@ -1,6 +1,14 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + from django.db import models from django_mailer import constants, managers -import datetime +try: + from django.utils.timezone import now +except ImportError: + import datetime + now = datetime.datetime.now PRIORITIES = ( @@ -19,19 +27,19 @@ class Message(models.Model): """ An email message. - + The ``to_address``, ``from_address`` and ``subject`` fields are merely for easy of access for these common values. The ``encoded_message`` field contains the entire encoded email message ready to be sent to an SMTP connection. - + """ to_address = models.CharField(max_length=200) from_address = models.CharField(max_length=200) subject = models.CharField(max_length=255) encoded_message = models.TextField() - date_created = models.DateTimeField(default=datetime.datetime.now) + date_created = models.DateTimeField(default=now) class Meta: ordering = ('date_created',) @@ -43,17 +51,17 @@ def __unicode__(self): class QueuedMessage(models.Model): """ A queued message. - + Messages in the queue can be prioritised so that the higher priority messages are sent first (secondarily sorted by the oldest message). - + """ message = models.OneToOneField(Message, editable=False) priority = models.PositiveSmallIntegerField(choices=PRIORITIES, default=constants.PRIORITY_NORMAL) deferred = models.DateTimeField(null=True, blank=True) retries = models.PositiveIntegerField(default=0) - date_queued = models.DateTimeField(default=datetime.datetime.now) + date_queued = models.DateTimeField(default=now) objects = managers.QueueManager() @@ -61,20 +69,20 @@ class Meta: ordering = ('priority', 'date_queued') def defer(self): - self.deferred = datetime.datetime.now() + self.deferred = now() self.save() class Blacklist(models.Model): """ A blacklisted email address. - + Messages attempted to be sent to e-mail addresses which appear on this blacklist will be skipped entirely. - + """ email = models.EmailField(max_length=200) - date_added = models.DateTimeField(default=datetime.datetime.now) + date_added = models.DateTimeField(default=now) class Meta: ordering = ('-date_added',) @@ -85,11 +93,11 @@ class Meta: class Log(models.Model): """ A log used to record the activity of a queued message. - + """ message = models.ForeignKey(Message, editable=False) result = models.PositiveSmallIntegerField(choices=RESULT_CODES) - date = models.DateTimeField(default=datetime.datetime.now) + date = models.DateTimeField(default=now) log_message = models.TextField() class Meta: diff --git a/django_mailer/templates/admin/django_mailer/message/change_form.html b/django_mailer/templates/admin/django_mailer/message/change_form.html new file mode 100644 index 00000000..53a1ab75 --- /dev/null +++ b/django_mailer/templates/admin/django_mailer/message/change_form.html @@ -0,0 +1,16 @@ +{% extends "admin/change_form.html" %} +{% load i18n admin_urls %} +{% load url from future %} + +{% block object-tools-items %} +
  • + + + + Show + +{{block.super}} +
  • {% endblock %} + diff --git a/django_mailer/templates/django_mailer/html_detail.html b/django_mailer/templates/django_mailer/html_detail.html new file mode 100644 index 00000000..fca47b0e --- /dev/null +++ b/django_mailer/templates/django_mailer/html_detail.html @@ -0,0 +1 @@ +{{msg_html|safe}} diff --git a/django_mailer/templates/django_mailer/message_detail.html b/django_mailer/templates/django_mailer/message_detail.html new file mode 100644 index 00000000..7ccd87e2 --- /dev/null +++ b/django_mailer/templates/django_mailer/message_detail.html @@ -0,0 +1,44 @@ +{% extends "admin/change_form.html" %} +{% load url from future %} +{% block breadcrumbs %}{% endblock breadcrumbs %} +{% block content_title %} +

    Message {{object.id}}

    +{% endblock content_title %} +{% block content %} +
    + {{subject}} +
    +
    + <{{from.0}}>{{from.1}}
    +
    +
    + {% for x in to %}<{{x.0}}>{{x.1}} {% endfor %}
    +
    +
    + {% for x in cc %}<{{x.0}}>{{x.1}} {% endfor %}
    +
    + + {% if msg_html %} +
    +

    HTML

    + +
    + {% endif %} + {% if msg_text %} +
    +

    Text

    + {{msg_text|linebreaks}} +
    + {% endif %} + {% if attachments %} +
    +

    Attachments

    + {% for file in attachments %} + + {% endfor %} +
    + {% endif %} +{% endblock content %} diff --git a/django_mailer/templates/django_mailer/message_list.html b/django_mailer/templates/django_mailer/message_list.html new file mode 100644 index 00000000..50f1749f --- /dev/null +++ b/django_mailer/templates/django_mailer/message_list.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% load url from future %} +{% load i18n %} +{% block content %} +

    {% trans "Message List" %}

    + + + {% for mail in object_list %} + + + + + + + + {% endfor %} +
    {% trans "id" %}{% trans "From" %}{% trans "To" %}{% trans "Subject" %}{% trans "Created" %}
    {{mail.pk}}{{mail.from_address}}{{mail.to_address}}{{mail.subject}}{{mail.date_created}}
    + +{% if is_paginated %} + {% if page_obj.has_previous %} + + {% trans "Previous" %} + {% endif %} + + {% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}. + + {% if page_obj.has_next %} + + {% trans "Next" %} + {% endif %} +{% endif %} + + {% endblock content %} diff --git a/django_mailer/testapp/__init__.py b/django_mailer/testapp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/django_mailer/testapp/models.py b/django_mailer/testapp/models.py new file mode 100644 index 00000000..e69de29b diff --git a/django_mailer/testapp/requirements.txt b/django_mailer/testapp/requirements.txt new file mode 100644 index 00000000..5a80988e --- /dev/null +++ b/django_mailer/testapp/requirements.txt @@ -0,0 +1,2 @@ +django==1.4.3 + diff --git a/django_mailer/testapp/settings.py b/django_mailer/testapp/settings.py new file mode 100644 index 00000000..232199f8 --- /dev/null +++ b/django_mailer/testapp/settings.py @@ -0,0 +1,19 @@ + +EMAIL_PORT = 1025 +ROOT_URLCONF = 'django_mailer.apptest.urls' + +SECRET_KEY = 'yo secret yo' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'django_mailer.sqlite', + }, +} + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django_mailer', + 'django_mailer.testapp' +) diff --git a/django_mailer/testapp/templates/base.html b/django_mailer/testapp/templates/base.html new file mode 100644 index 00000000..24132993 --- /dev/null +++ b/django_mailer/testapp/templates/base.html @@ -0,0 +1,3 @@ +{% block title %}{% endblock %} + +{% block content %}{% endblock %} diff --git a/django_mailer/testapp/tests/__init__.py b/django_mailer/testapp/tests/__init__.py new file mode 100644 index 00000000..c28d8ac5 --- /dev/null +++ b/django_mailer/testapp/tests/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +from commands import TestCommands +from engine import LockTest #COULD DROP THIS TEST +from backend import TestBackend diff --git a/django_mailer/tests/backend.py b/django_mailer/testapp/tests/backend.py similarity index 67% rename from django_mailer/tests/backend.py rename to django_mailer/testapp/tests/backend.py index df5e785a..c747ee5c 100644 --- a/django_mailer/tests/backend.py +++ b/django_mailer/testapp/tests/backend.py @@ -1,7 +1,11 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + from django.conf import settings as django_settings from django.core import mail from django_mailer import models, constants, queue_email_message -from django_mailer.tests.base import MailerTestCase +from base import MailerTestCase class TestBackend(MailerTestCase): @@ -43,40 +47,72 @@ def testQueuedMessagePriorities(self): from_email='mail_from@abc.com', to=['mail_to@abc.com'], headers={'X-Mail-Queue-Priority': 'high'}) self.send_message(msg) - + # low priority message msg = mail.EmailMessage(subject='subject', body='body', from_email='mail_from@abc.com', to=['mail_to@abc.com'], headers={'X-Mail-Queue-Priority': 'low'}) self.send_message(msg) - + # normal priority message msg = mail.EmailMessage(subject='subject', body='body', from_email='mail_from@abc.com', to=['mail_to@abc.com'], headers={'X-Mail-Queue-Priority': 'normal'}) self.send_message(msg) - + # normal priority message (no explicit priority header) msg = mail.EmailMessage(subject='subject', body='body', from_email='mail_from@abc.com', to=['mail_to@abc.com']) self.send_message(msg) - + qs = models.QueuedMessage.objects.high_priority() self.assertEqual(qs.count(), 1) queued_message = qs[0] self.assertEqual(queued_message.priority, constants.PRIORITY_HIGH) - + qs = models.QueuedMessage.objects.low_priority() self.assertEqual(qs.count(), 1) queued_message = qs[0] self.assertEqual(queued_message.priority, constants.PRIORITY_LOW) - + qs = models.QueuedMessage.objects.normal_priority() self.assertEqual(qs.count(), 2) for queued_message in qs: self.assertEqual(queued_message.priority, constants.PRIORITY_NORMAL) + def testUnicodeQueuedMessage(self): + """ + Checks that we capture unicode errors on mail + """ + from django.core.management import call_command + msg = mail.EmailMessage(subject='subject', body='body', + from_email=u'juan.lópez@abc.com', to=['mail_to@abc.com'], + headers={'X-Mail-Queue-Priority': 'normal'}) + self.send_message(msg) + queued_messages = models.QueuedMessage.objects.all() + self.assertEqual(queued_messages.count(), 1) + call_command('send_mail', verbosity='0') + num_errors = models.Log.objects.filter(result=constants.RESULT_FAILED).count() + self.assertEqual(num_errors, 1) + + def testUnicodePriorityMessage(self): + """ + Checks that we capture unicode errors on mail on priority. + It's hard to check as by definiton priority email does not Logs its + contents. + """ + from django.core.management import call_command + msg = mail.EmailMessage(subject=u'á subject', body='body', + from_email=u'juan.lópez@abc.com', to=[u'únñac@abc.com'], + headers={'X-Mail-Queue-Priority': 'now'}) + self.send_message(msg) + queued_messages = models.QueuedMessage.objects.all() + self.assertEqual(queued_messages.count(), 0) + call_command('send_mail', verbosity='0') + num_errors = models.Log.objects.filter(result=constants.RESULT_FAILED).count() + self.assertEqual(num_errors, 0) + def testSendMessageNowPriority(self): # NOW priority message msg = mail.EmailMessage(subject='subject', body='body', @@ -86,4 +122,3 @@ def testSendMessageNowPriority(self): queued_messages = models.QueuedMessage.objects.all() self.assertEqual(queued_messages.count(), 0) - self.assertEqual(len(mail.outbox), 1) diff --git a/django_mailer/testapp/tests/base.py b/django_mailer/testapp/tests/base.py new file mode 100644 index 00000000..0d06827e --- /dev/null +++ b/django_mailer/testapp/tests/base.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +from django.core import mail +from django.test import TestCase +from django_mailer import queue_email_message +from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail import backends + +class FakeConnection(object): + """ + A fake SMTP connection which diverts emails to the test buffer rather than + sending. + + """ + def sendmail(self, *args, **kwargs): + """ + Divert an email to the test buffer. + + """ + #FUTURE: the EmailMessage attributes could be found by introspecting + # the encoded message. + message = mail.EmailMessage('SUBJECT', 'BODY', 'FROM', ['TO']) + mail.outbox.append(message) + + +class TestEmailBackend(BaseEmailBackend): + ''' + An EmailBackend used in place of the default + django.core.mail.backends.smtp.EmailBackend. + + ''' + def __init__(self, fail_silently=False, **kwargs): + super(TestEmailBackend, self).__init__(fail_silently=fail_silently) + self.connection = FakeConnection() + + def send_messages(self, email_messages): + pass + + +class MailerTestCase(TestCase): + """ + A base class for Django Mailer test cases which diverts emails to the test + buffer and provides some helper methods. + + """ + #def setUp(self): + #self.saved_email_backend = backends.smtp.EmailBackend + #backends.smtp.EmailBackend = TestEmailBackend + + #def tearDown(self): + #backends.smtp.EmailBackend = self.saved_email_backend + + def queue_message(self, subject='test', message='a test message', + from_email='sender@djangomailer', + recipient_list=['recipient@djangomailer'], + priority=None): + email_message = mail.EmailMessage(subject, message, from_email, + recipient_list) + return queue_email_message(email_message, priority=priority) diff --git a/django_mailer/testapp/tests/commands.py b/django_mailer/testapp/tests/commands.py new file mode 100644 index 00000000..ada2e2b9 --- /dev/null +++ b/django_mailer/testapp/tests/commands.py @@ -0,0 +1,120 @@ +from django.core import mail +from django.core.management import call_command +from django_mailer import models +from base import MailerTestCase +import datetime +try: + from django.utils.timezone import now +except ImportError: + now = datetime.datetime.now + + +class TestCommands(MailerTestCase): + """ + A test case for management commands provided by django-mailer. + + """ + def test_send_mail(self): + """ + The ``send_mail`` command initiates the sending of messages in the + queue. + + """ + # No action is taken if there are no messages. + call_command('send_mail', verbosity='0') + # Any (non-deferred) queued messages will be sent. + self.queue_message() + self.queue_message() + self.queue_message(subject='deferred') + models.QueuedMessage.objects\ + .filter(message__subject__startswith='deferred')\ + .update(deferred=now()) + queued_messages = models.QueuedMessage.objects.all() + self.assertEqual(queued_messages.count(), 3) + self.assertEqual(len(mail.outbox), 0) + call_command('send_mail', verbosity='0') + self.assertEqual(queued_messages.count(), 1) + + def test_retry_deferred(self): + """ + The ``retry_deferred`` command places deferred messages back in the + queue. + + """ + self.queue_message() + self.queue_message(subject='deferred') + self.queue_message(subject='deferred 2') + self.queue_message(subject='deferred 3') + models.QueuedMessage.objects\ + .filter(message__subject__startswith='deferred')\ + .update(deferred=now()) + non_deferred_messages = models.QueuedMessage.objects.non_deferred() + # Deferred messages are returned to the queue (nothing is sent). + self.assertEqual(non_deferred_messages.count(), 1) + call_command('retry_deferred', verbosity='0') + self.assertEqual(non_deferred_messages.count(), 4) + self.assertEqual(len(mail.outbox), 0) + # Check the --max-retries logic. + models.QueuedMessage.objects\ + .filter(message__subject='deferred')\ + .update(deferred=now(), retries=2) + models.QueuedMessage.objects\ + .filter(message__subject='deferred 2')\ + .update(deferred=now(), retries=3) + models.QueuedMessage.objects\ + .filter(message__subject='deferred 3')\ + .update(deferred=now(), retries=4) + self.assertEqual(non_deferred_messages.count(), 1) + call_command('retry_deferred', verbosity='0', max_retries=3) + self.assertEqual(non_deferred_messages.count(), 3) + + def test_status_mail(self): + """ + The ``status_mail`` should return a string that matches: + (?P\d+)/(?P\d+)/(?P\d+) + """ + import re + import sys + from cStringIO import StringIO + import time + + re_string = r"(?P\d+)/(?P\d+)/(?P\d+)" + p = re.compile(re_string) + + self.queue_message(subject="test") + self.queue_message(subject='deferred') + self.queue_message(subject='deferred 2') + self.queue_message(subject='deferred 3') + models.QueuedMessage.objects\ + .filter(message__subject__startswith='deferred')\ + .update(deferred=now()) + non_deferred_messages = models.QueuedMessage.objects.non_deferred() + time.sleep(1) + # Deferred messages are returned to the queue (nothing is sent). + out, sys.stdout = sys.stdout, StringIO() + with self.assertRaises(SystemExit) as cm: + call_command('status_mail', verbosity='0') + sys.stdout.seek(0) + result = sys.stdout.read() + m = p.match(result) + sys.stdout = out + self.assertTrue(m, "Output does not include expected r.e.") + v = m.groupdict() + self.assertTrue(v['queued'], "1") + self.assertEqual(v['deferred'], "3") + self.assertTrue(int(v['seconds'])>=1) + + def test_cleanup_mail(self): + """ + The ``cleanup_mail`` command deletes mails older than a specified + amount of days + """ + today = datetime.date.today() + self.assertEqual(models.Message.objects.count(), 0) + #new message (not to be deleted) + models.Message.objects.create() + prev = today - datetime.timedelta(31) + # new message (old) + models.Message.objects.create(date_created=prev) + call_command('cleanup_mail', days=30) + self.assertEqual(models.Message.objects.count(), 1) diff --git a/django_mailer/tests/engine.py b/django_mailer/testapp/tests/engine.py similarity index 100% rename from django_mailer/tests/engine.py rename to django_mailer/testapp/tests/engine.py diff --git a/django_mailer/testapp/urls.py b/django_mailer/testapp/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/django_mailer/tests/__init__.py b/django_mailer/tests/__init__.py deleted file mode 100644 index 14768536..00000000 --- a/django_mailer/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from django_mailer.tests.commands import TestCommands -from django_mailer.tests.engine import LockTest #COULD DROP THIS TEST -from django_mailer.tests.backend import TestBackend \ No newline at end of file diff --git a/django_mailer/tests/base.py b/django_mailer/tests/base.py deleted file mode 100644 index 21faa239..00000000 --- a/django_mailer/tests/base.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.core import mail -from django.test import TestCase -from django_mailer import queue_email_message -try: - from django.core.mail import backends - EMAIL_BACKEND_SUPPORT = True -except ImportError: - # Django version < 1.2 - EMAIL_BACKEND_SUPPORT = False - -class FakeConnection(object): - """ - A fake SMTP connection which diverts emails to the test buffer rather than - sending. - - """ - def sendmail(self, *args, **kwargs): - """ - Divert an email to the test buffer. - - """ - #FUTURE: the EmailMessage attributes could be found by introspecting - # the encoded message. - message = mail.EmailMessage('SUBJECT', 'BODY', 'FROM', ['TO']) - mail.outbox.append(message) - - -if EMAIL_BACKEND_SUPPORT: - class TestEmailBackend(backends.base.BaseEmailBackend): - ''' - An EmailBackend used in place of the default - django.core.mail.backends.smtp.EmailBackend. - - ''' - def __init__(self, fail_silently=False, **kwargs): - super(TestEmailBackend, self).__init__(fail_silently=fail_silently) - self.connection = FakeConnection() - - def send_messages(self, email_messages): - pass - - -class MailerTestCase(TestCase): - """ - A base class for Django Mailer test cases which diverts emails to the test - buffer and provides some helper methods. - - """ - def setUp(self): - if EMAIL_BACKEND_SUPPORT: - self.saved_email_backend = backends.smtp.EmailBackend - backends.smtp.EmailBackend = TestEmailBackend - else: - connection = mail.SMTPConnection - if hasattr(connection, 'connection'): - connection.pretest_connection = connection.connection - connection.connection = FakeConnection() - - def tearDown(self): - if EMAIL_BACKEND_SUPPORT: - backends.smtp.EmailBackend = self.saved_email_backend - else: - connection = mail.SMTPConnection - if hasattr(connection, 'pretest_connection'): - connection.connection = connection.pretest_connection - - def queue_message(self, subject='test', message='a test message', - from_email='sender@djangomailer', - recipient_list=['recipient@djangomailer'], - priority=None): - email_message = mail.EmailMessage(subject, message, from_email, - recipient_list) - return queue_email_message(email_message, priority=priority) diff --git a/django_mailer/tests/commands.py b/django_mailer/tests/commands.py deleted file mode 100644 index 8c708654..00000000 --- a/django_mailer/tests/commands.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.core import mail -from django.core.management import call_command -from django_mailer import models -from django_mailer.tests.base import MailerTestCase -import datetime - - -class TestCommands(MailerTestCase): - """ - A test case for management commands provided by django-mailer. - - """ - def test_send_mail(self): - """ - The ``send_mail`` command initiates the sending of messages in the - queue. - - """ - # No action is taken if there are no messages. - call_command('send_mail', verbosity='0') - # Any (non-deferred) queued messages will be sent. - self.queue_message() - self.queue_message() - self.queue_message(subject='deferred') - models.QueuedMessage.objects\ - .filter(message__subject__startswith='deferred')\ - .update(deferred=datetime.datetime.now()) - queued_messages = models.QueuedMessage.objects.all() - self.assertEqual(queued_messages.count(), 3) - self.assertEqual(len(mail.outbox), 0) - call_command('send_mail', verbosity='0') - self.assertEqual(queued_messages.count(), 1) - self.assertEqual(len(mail.outbox), 2) - - def test_retry_deferred(self): - """ - The ``retry_deferred`` command places deferred messages back in the - queue. - - """ - self.queue_message() - self.queue_message(subject='deferred') - self.queue_message(subject='deferred 2') - self.queue_message(subject='deferred 3') - models.QueuedMessage.objects\ - .filter(message__subject__startswith='deferred')\ - .update(deferred=datetime.datetime.now()) - non_deferred_messages = models.QueuedMessage.objects.non_deferred() - # Deferred messages are returned to the queue (nothing is sent). - self.assertEqual(non_deferred_messages.count(), 1) - call_command('retry_deferred', verbosity='0') - self.assertEqual(non_deferred_messages.count(), 4) - self.assertEqual(len(mail.outbox), 0) - # Check the --max-retries logic. - models.QueuedMessage.objects\ - .filter(message__subject='deferred')\ - .update(deferred=datetime.datetime.now(), retries=2) - models.QueuedMessage.objects\ - .filter(message__subject='deferred 2')\ - .update(deferred=datetime.datetime.now(), retries=3) - models.QueuedMessage.objects\ - .filter(message__subject='deferred 3')\ - .update(deferred=datetime.datetime.now(), retries=4) - self.assertEqual(non_deferred_messages.count(), 1) - call_command('retry_deferred', verbosity='0', max_retries=3) - self.assertEqual(non_deferred_messages.count(), 3) diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..35c11ffb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-mailer2.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-mailer2.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-mailer2" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-mailer2" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..3507c8b1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +# +# django-mailer2 documentation build configuration file, created by +# sphinx-quickstart on Mon Feb 25 00:09:36 2013. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.todo'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'django-mailer2' +copyright = u'2013, Tauber, Beaven & APSL' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = 'master' +# The full version, including alpha/beta/rc tags. +release = 'master' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-mailer2doc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'django-mailer2.tex', u'django-mailer2 Documentation', + u'Tauber, Beaven \\& APSL', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'django-mailer2', u'django-mailer2 Documentation', + [u'Tauber, Beaven & APSL'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'django-mailer2', u'django-mailer2 Documentation', + u'Tauber, Beaven & APSL', 'django-mailer2', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/docs/index.txt b/docs/index.rst similarity index 62% rename from docs/index.txt rename to docs/index.rst index dd9700fa..f59d246b 100644 --- a/docs/index.txt +++ b/docs/index.rst @@ -17,7 +17,13 @@ Contents: Django Mailer fork ================== -django-mailer-2 is a fork of James Tauber's `django-mailer`__. +django-mailer-2 is a fork form Chris Beaven fort to of James Tauber's +`django-mailer`.__ + +This document is readthedocs version of the fork that Chris and James made +the original document with some additional information. + +comments. .. __: http://github.com/jtauber/django-mailer @@ -37,6 +43,11 @@ technically live side by side in harmony. One of the motivations in doing this was to make the transition simpler for projects which are using django-mailer (or to transition back, if someone doesn't like this one). +I made an additional fork as I need to correct some bugs related to unicode +mail and add some interesting patches as the one which allows you to remove +mails. + + Differences ----------- @@ -51,9 +62,27 @@ Some of the larger differences in django-mailer-2: * It provides a hook to override (aka "monkey patch") the Django ``send_mail``, ``mail_admins`` and ``mail_manager`` functions. +* Added a management command to remove old e-mails, so the database does not + increase so much. + +* Added a new testing procedure, so you can run the tests without having to + install and configure a Django application. + +* Added some cron templates ein `bin` folder to help you to configure the + cron. + +* Improved admin configuration. + +* Added a demo project, which shows how we can retrieve an email stored in + the database and shows django-mailer in the admin. + Credit ------ At the time of the fork, the primary authors of django-mailer were James Tauber and Brian Rosner. The additional contributors included Michael Trier, Doug -Napoleone and Jannis Leidel. \ No newline at end of file +Napoleone and Jannis Leidel. + +Original branch and the django-mailer-2 hard work comes from Chris Beaven. + + diff --git a/docs/install.txt b/docs/install.rst similarity index 100% rename from docs/install.txt rename to docs/install.rst diff --git a/docs/settings.txt b/docs/settings.rst similarity index 100% rename from docs/settings.txt rename to docs/settings.rst diff --git a/docs/usage.txt b/docs/usage.rst similarity index 81% rename from docs/usage.txt rename to docs/usage.rst index 5d719dcc..70f25533 100644 --- a/docs/usage.txt +++ b/docs/usage.rst @@ -60,10 +60,10 @@ or all managers as defined in the ``MANAGERS`` setting by calling:: mail_managers(subject, message_body) -Clear Queue With Command Extensions +Command Extensions =================================== -With mailer in your INSTALLED_APPS, there will be two new manage.py commands +With mailer in your INSTALLED_APPS, there will be four new manage.py commands you can run: * ``send_mail`` will clear the current message queue. If there are any @@ -73,16 +73,26 @@ you can run: * ``retry_deferred`` will move any deferred mail back into the normal queue (so it will be attempted again on the next ``send_mail``). + * ``cleanup_mail`` will delete mails created before an X number of days + (defaults to 90). + + * ``status_mail`` the intent of this commant is to allow systems as nagios to + be able to ask the queue about its status. It returns as string with than + can be parses as ``(?P\d+)/(?P\d+)/(?P\d+)`` + You may want to set these up via cron to run regularly:: * * * * * (cd $PROJECT; python manage.py send_mail >> $PROJECT/cron_mail.log 2>&1) 0,20,40 * * * * (cd $PROJECT; python manage.py retry_deferred >> $PROJECT/cron_mail_deferred.log 2>&1) + 0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 >> $PROJECT/cron_mail_cleanup.log 2>&1) -This attempts to send mail every minute with a retry on failure every 20 minutes. +This attempts to send mail every minute with a retry on failure every 20 minutes +and will run a cleanup task every day cleaning all the messaged created before +30 days. ``manage.py send_mail`` uses a lock file in case clearing the queue takes longer than the interval between calling ``manage.py send_mail``. Note that if your project lives inside a virtualenv, you also have to execute this command from the virtualenv. The same, naturally, applies also if you're -executing it with cron. \ No newline at end of file +executing it with cron. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ca2be0c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pyzmail diff --git a/runtests.py b/runtests.py new file mode 100755 index 00000000..eab2ce8b --- /dev/null +++ b/runtests.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- + +import os +import sys + +parent = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, parent) +os.environ['DJANGO_SETTINGS_MODULE'] = 'django_mailer.testapp.settings' + +# Django 1.7 and later requires a separate .setup() call +import django +try: + django.setup() +except AttributeError: + pass + +from django.test.simple import DjangoTestSuiteRunner + + +def runtests(*test_args): + test_args = test_args or ['testapp'] + parent = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, parent) + runner = DjangoTestSuiteRunner(verbosity=1, interactive=True, + failfast=False) + failures = runner.run_tests(test_args) + sys.exit(failures) + + +if __name__ == '__main__': + runtests() diff --git a/setup.py b/setup.py index 423d359a..01d47268 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,26 @@ -from distutils.core import setup -from django_mailer import get_version +#!/usr/bin/env python +# encoding: utf-8 +# ---------------------------------------------------------------------------- +from setuptools import setup +from django_mailer import get_version setup( name='django-mailer-2', version=get_version(), description=("A reusable Django app for queueing the sending of email " - "(forked from James Tauber's django-mailer)"), - long_description=open('docs/usage.txt').read(), - author='Chris Beaven', - author_email='smileychris@gmail.com', - url='http://github.com/SmileyChris/django-mailer-2', + "(forked aaloy on a frok from James Tauber's django-mailer)"), + long_description=open('docs/usage.rst').read(), + author='Antoni Aloy', + author_email='antoni.aloy@gmail.com', + url='http://github.com/APSL/django-mailer-2', + install_requires = ["pyzmail", ], packages=[ 'django_mailer', 'django_mailer.management', 'django_mailer.management.commands', - 'django_mailer.tests', ], + include_package_data=True, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment',