123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- # -*- coding: utf-8 -*-
- from __future__ import unicode_literals
-
- import os
-
- from collections import namedtuple
- from uuid import uuid4
-
- from django.core.mail import EmailMessage, EmailMultiAlternatives
- from django.db import models
- from django.template import Context, Template
- from django.utils.encoding import python_2_unicode_compatible
- from django.utils.translation import pgettext_lazy
- from django.utils.translation import ugettext_lazy as _
- from django.utils import timezone
- from jsonfield import JSONField
-
- from post_office import cache
- from post_office.fields import CommaSeparatedEmailField
-
- from .compat import text_type, smart_text
- from .connections import connections
- from .settings import context_field_class, get_log_level
- from .validators import validate_email_with_name, validate_template_syntax
-
-
- PRIORITY = namedtuple('PRIORITY', 'low medium high now')._make(range(4))
- STATUS = namedtuple('STATUS', 'sent failed queued')._make(range(3))
-
-
- @python_2_unicode_compatible
- class Email(models.Model):
- """
- A model to hold email information.
- """
-
- PRIORITY_CHOICES = [(PRIORITY.low, _("low")), (PRIORITY.medium, _("medium")),
- (PRIORITY.high, _("high")), (PRIORITY.now, _("now"))]
- STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed")),
- (STATUS.queued, _("queued"))]
-
- from_email = models.CharField(_("Email From"), max_length=254,
- validators=[validate_email_with_name])
- to = CommaSeparatedEmailField(_("Email To"))
- cc = CommaSeparatedEmailField(_("Cc"))
- bcc = CommaSeparatedEmailField(_("Bcc"))
- subject = models.CharField(_("Subject"), max_length=989, blank=True)
- message = models.TextField(_("Message"), blank=True)
- html_message = models.TextField(_("HTML Message"), blank=True)
- """
- Emails with 'queued' status will get processed by ``send_queued`` command.
- Status field will then be set to ``failed`` or ``sent`` depending on
- whether it's successfully delivered.
- """
- status = models.PositiveSmallIntegerField(
- _("Status"),
- choices=STATUS_CHOICES, db_index=True,
- blank=True, null=True)
- priority = models.PositiveSmallIntegerField(_("Priority"),
- choices=PRIORITY_CHOICES,
- blank=True, null=True)
- created = models.DateTimeField(auto_now_add=True, db_index=True)
- last_updated = models.DateTimeField(db_index=True, auto_now=True)
- scheduled_time = models.DateTimeField(_('The scheduled sending time'),
- blank=True, null=True, db_index=True)
- headers = JSONField(_('Headers'), blank=True, null=True)
- template = models.ForeignKey('post_office.EmailTemplate', blank=True,
- null=True, verbose_name=_('Email template'),
- on_delete=models.CASCADE)
- context = context_field_class(_('Context'), blank=True, null=True)
- backend_alias = models.CharField(_('Backend alias'), blank=True, default='',
- max_length=64)
-
- class Meta:
- app_label = 'post_office'
- verbose_name = pgettext_lazy("Email address", "Email")
- verbose_name_plural = pgettext_lazy("Email addresses", "Emails")
-
- def __init__(self, *args, **kwargs):
- super(Email, self).__init__(*args, **kwargs)
- self._cached_email_message = None
-
- def __str__(self):
- return u'%s' % self.to
-
- def email_message(self):
- """
- Returns Django EmailMessage object for sending.
- """
- if self._cached_email_message:
- return self._cached_email_message
-
- return self.prepare_email_message()
-
- def prepare_email_message(self):
- """
- Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object,
- depending on whether html_message is empty.
- """
- subject = smart_text(self.subject)
-
- if self.template is not None:
- _context = Context(self.context)
- subject = Template(self.template.subject).render(_context)
- message = Template(self.template.content).render(_context)
- html_message = Template(self.template.html_content).render(_context)
-
- else:
- subject = self.subject
- message = self.message
- html_message = self.html_message
-
- connection = connections[self.backend_alias or 'default']
-
- if html_message:
- msg = EmailMultiAlternatives(
- subject=subject, body=message, from_email=self.from_email,
- to=self.to, bcc=self.bcc, cc=self.cc,
- headers=self.headers, connection=connection)
- msg.attach_alternative(html_message, "text/html")
- else:
- msg = EmailMessage(
- subject=subject, body=message, from_email=self.from_email,
- to=self.to, bcc=self.bcc, cc=self.cc,
- headers=self.headers, connection=connection)
-
- for attachment in self.attachments.all():
- msg.attach(attachment.name, attachment.file.read(), mimetype=attachment.mimetype or None)
- attachment.file.close()
-
- self._cached_email_message = msg
- return msg
-
- def dispatch(self, log_level=None,
- disconnect_after_delivery=True, commit=True):
- """
- Sends email and log the result.
- """
- try:
- self.email_message().send()
- status = STATUS.sent
- message = ''
- exception_type = ''
- except Exception as e:
- status = STATUS.failed
- message = str(e)
- exception_type = type(e).__name__
-
- # If run in a bulk sending mode, reraise and let the outer
- # layer handle the exception
- if not commit:
- raise
-
- if commit:
- self.status = status
- self.save(update_fields=['status'])
-
- if log_level is None:
- log_level = get_log_level()
-
- # If log level is 0, log nothing, 1 logs only sending failures
- # and 2 means log both successes and failures
- if log_level == 1:
- if status == STATUS.failed:
- self.logs.create(status=status, message=message,
- exception_type=exception_type)
- elif log_level == 2:
- self.logs.create(status=status, message=message,
- exception_type=exception_type)
-
- return status
-
- def save(self, *args, **kwargs):
- self.full_clean()
- return super(Email, self).save(*args, **kwargs)
-
-
- @python_2_unicode_compatible
- class Log(models.Model):
- """
- A model to record sending email sending activities.
- """
-
- STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed"))]
-
- email = models.ForeignKey(Email, editable=False, related_name='logs',
- verbose_name=_('Email address'), on_delete=models.CASCADE)
- date = models.DateTimeField(auto_now_add=True)
- status = models.PositiveSmallIntegerField(_('Status'), choices=STATUS_CHOICES)
- exception_type = models.CharField(_('Exception type'), max_length=255, blank=True)
- message = models.TextField(_('Message'))
-
- class Meta:
- app_label = 'post_office'
- verbose_name = _("Log")
- verbose_name_plural = _("Logs")
-
- def __str__(self):
- return text_type(self.date)
-
-
- class EmailTemplateManager(models.Manager):
- def get_by_natural_key(self, name, language, default_template):
- return self.get(name=name, language=language, default_template=default_template)
-
-
- @python_2_unicode_compatible
- class EmailTemplate(models.Model):
- """
- Model to hold template information from db
- """
- name = models.CharField(_('Name'), max_length=255, help_text=_("e.g: 'welcome_email'"))
- description = models.TextField(_('Description'), blank=True,
- help_text=_("Description of this template."))
- created = models.DateTimeField(auto_now_add=True)
- last_updated = models.DateTimeField(auto_now=True)
- subject = models.CharField(max_length=255, blank=True,
- verbose_name=_("Subject"), validators=[validate_template_syntax])
- content = models.TextField(blank=True,
- verbose_name=_("Content"), validators=[validate_template_syntax])
- html_content = models.TextField(blank=True,
- verbose_name=_("HTML content"), validators=[validate_template_syntax])
- language = models.CharField(max_length=12,
- verbose_name=_("Language"),
- help_text=_("Render template in alternative language"),
- default='', blank=True)
- default_template = models.ForeignKey('self', related_name='translated_templates',
- null=True, default=None, verbose_name=_('Default template'), on_delete=models.CASCADE)
-
- objects = EmailTemplateManager()
-
- class Meta:
- app_label = 'post_office'
- unique_together = ('name', 'language', 'default_template')
- verbose_name = _("Email Template")
- verbose_name_plural = _("Email Templates")
- ordering = ['name']
-
- def __str__(self):
- return u'%s %s' % (self.name, self.language)
-
- def natural_key(self):
- return (self.name, self.language, self.default_template)
-
- def save(self, *args, **kwargs):
- # If template is a translation, use default template's name
- if self.default_template and not self.name:
- self.name = self.default_template.name
-
- template = super(EmailTemplate, self).save(*args, **kwargs)
- cache.delete(self.name)
- return template
-
-
- def get_upload_path(instance, filename):
- """Overriding to store the original filename"""
- if not instance.name:
- instance.name = filename # set original filename
- date = timezone.now().date()
- filename = '{name}.{ext}'.format(name=uuid4().hex,
- ext=filename.split('.')[-1])
-
- return os.path.join('post_office_attachments', str(date.year),
- str(date.month), str(date.day), filename)
-
-
- @python_2_unicode_compatible
- class Attachment(models.Model):
- """
- A model describing an email attachment.
- """
- file = models.FileField(_('File'), upload_to=get_upload_path)
- name = models.CharField(_('Name'), max_length=255, help_text=_("The original filename"))
- emails = models.ManyToManyField(Email, related_name='attachments',
- verbose_name=_('Email addresses'))
- mimetype = models.CharField(max_length=255, default='', blank=True)
-
- class Meta:
- app_label = 'post_office'
- verbose_name = _("Attachment")
- verbose_name_plural = _("Attachments")
-
- def __str__(self):
- return self.name
|