123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- from multiprocessing import Pool
- from multiprocessing.dummy import Pool as ThreadPool
-
- from django.conf import settings
- from django.core.exceptions import ValidationError
- from django.db import connection as db_connection
- from django.db.models import Q
- from django.template import Context, Template
- from django.utils.timezone import now
-
- from .connections import connections
- from .models import Email, EmailTemplate, Log, PRIORITY, STATUS
- from .settings import (get_available_backends, get_batch_size,
- get_log_level, get_sending_order, get_threads_per_process)
- from .utils import (get_email_template, parse_emails, parse_priority,
- split_emails, create_attachments)
- from .logutils import setup_loghandlers
-
-
- logger = setup_loghandlers("INFO")
-
-
- def create(sender, recipients=None, cc=None, bcc=None, subject='', message='',
- html_message='', context=None, scheduled_time=None, headers=None,
- template=None, priority=None, render_on_delivery=False, commit=True,
- backend=''):
- """
- Creates an email from supplied keyword arguments. If template is
- specified, email subject and content will be rendered during delivery.
- """
- priority = parse_priority(priority)
- status = None if priority == PRIORITY.now else STATUS.queued
-
- if recipients is None:
- recipients = []
- if cc is None:
- cc = []
- if bcc is None:
- bcc = []
- if context is None:
- context = ''
-
- # If email is to be rendered during delivery, save all necessary
- # information
- if render_on_delivery:
- email = Email(
- from_email=sender,
- to=recipients,
- cc=cc,
- bcc=bcc,
- scheduled_time=scheduled_time,
- headers=headers, priority=priority, status=status,
- context=context, template=template, backend_alias=backend
- )
-
- else:
-
- if template:
- subject = template.subject
- message = template.content
- html_message = template.html_content
-
- _context = Context(context or {})
- subject = Template(subject).render(_context)
- message = Template(message).render(_context)
- html_message = Template(html_message).render(_context)
-
- email = Email(
- from_email=sender,
- to=recipients,
- cc=cc,
- bcc=bcc,
- subject=subject,
- message=message,
- html_message=html_message,
- scheduled_time=scheduled_time,
- headers=headers, priority=priority, status=status,
- backend_alias=backend
- )
-
- if commit:
- email.save()
-
- return email
-
-
- def send(recipients=None, sender=None, template=None, context=None, subject='',
- message='', html_message='', scheduled_time=None, headers=None,
- priority=None, attachments=None, render_on_delivery=False,
- log_level=None, commit=True, cc=None, bcc=None, language='',
- backend=''):
-
- try:
- recipients = parse_emails(recipients)
- except ValidationError as e:
- raise ValidationError('recipients: %s' % e.message)
-
- try:
- cc = parse_emails(cc)
- except ValidationError as e:
- raise ValidationError('c: %s' % e.message)
-
- try:
- bcc = parse_emails(bcc)
- except ValidationError as e:
- raise ValidationError('bcc: %s' % e.message)
-
- if sender is None:
- sender = settings.DEFAULT_FROM_EMAIL
-
- priority = parse_priority(priority)
-
- if log_level is None:
- log_level = get_log_level()
-
- if not commit:
- if priority == PRIORITY.now:
- raise ValueError("send_many() can't be used with priority = 'now'")
- if attachments:
- raise ValueError("Can't add attachments with send_many()")
-
- if template:
- if subject:
- raise ValueError('You can\'t specify both "template" and "subject" arguments')
- if message:
- raise ValueError('You can\'t specify both "template" and "message" arguments')
- if html_message:
- raise ValueError('You can\'t specify both "template" and "html_message" arguments')
-
- # template can be an EmailTemplate instance or name
- if isinstance(template, EmailTemplate):
- template = template
- # If language is specified, ensure template uses the right language
- if language:
- if template.language != language:
- template = template.translated_templates.get(language=language)
- else:
- template = get_email_template(template, language)
-
- if backend and backend not in get_available_backends().keys():
- raise ValueError('%s is not a valid backend alias' % backend)
-
- email = create(sender, recipients, cc, bcc, subject, message, html_message,
- context, scheduled_time, headers, template, priority,
- render_on_delivery, commit=commit, backend=backend)
-
- if attachments:
- attachments = create_attachments(attachments)
- email.attachments.add(*attachments)
-
- if priority == PRIORITY.now:
- email.dispatch(log_level=log_level)
-
- return email
-
-
- def send_many(kwargs_list):
- """
- Similar to mail.send(), but this function accepts a list of kwargs.
- Internally, it uses Django's bulk_create command for efficiency reasons.
- Currently send_many() can't be used to send emails with priority = 'now'.
- """
- emails = []
- for kwargs in kwargs_list:
- emails.append(send(commit=False, **kwargs))
- Email.objects.bulk_create(emails)
-
-
- def get_queued():
- """
- Returns a list of emails that should be sent:
- - Status is queued
- - Has scheduled_time lower than the current time or None
- """
- return Email.objects.filter(status=STATUS.queued) \
- .select_related('template') \
- .filter(Q(scheduled_time__lte=now()) | Q(scheduled_time=None)) \
- .order_by(*get_sending_order()).prefetch_related('attachments')[:get_batch_size()]
-
-
- def send_queued(processes=1, log_level=None):
- """
- Sends out all queued mails that has scheduled_time less than now or None
- """
- queued_emails = get_queued()
- total_sent, total_failed = 0, 0
- total_email = len(queued_emails)
-
- logger.info('Started sending %s emails with %s processes.' %
- (total_email, processes))
-
- if log_level is None:
- log_level = get_log_level()
-
- if queued_emails:
-
- # Don't use more processes than number of emails
- if total_email < processes:
- processes = total_email
-
- if processes == 1:
- total_sent, total_failed = _send_bulk(queued_emails,
- uses_multiprocessing=False,
- log_level=log_level)
- else:
- email_lists = split_emails(queued_emails, processes)
-
- pool = Pool(processes)
- results = pool.map(_send_bulk, email_lists)
- pool.terminate()
-
- total_sent = sum([result[0] for result in results])
- total_failed = sum([result[1] for result in results])
- message = '%s emails attempted, %s sent, %s failed' % (
- total_email,
- total_sent,
- total_failed
- )
- logger.info(message)
- return (total_sent, total_failed)
-
-
- def _send_bulk(emails, uses_multiprocessing=True, log_level=None):
- # Multiprocessing does not play well with database connection
- # Fix: Close connections on forking process
- # https://groups.google.com/forum/#!topic/django-users/eCAIY9DAfG0
- if uses_multiprocessing:
- db_connection.close()
-
- if log_level is None:
- log_level = get_log_level()
-
- sent_emails = []
- failed_emails = [] # This is a list of two tuples (email, exception)
- email_count = len(emails)
-
- logger.info('Process started, sending %s emails' % email_count)
-
- def send(email):
- try:
- email.dispatch(log_level=log_level, commit=False,
- disconnect_after_delivery=False)
- sent_emails.append(email)
- logger.debug('Successfully sent email #%d' % email.id)
- except Exception as e:
- logger.debug('Failed to send email #%d' % email.id)
- failed_emails.append((email, e))
-
- # Prepare emails before we send these to threads for sending
- # So we don't need to access the DB from within threads
- for email in emails:
- # Sometimes this can fail, for example when trying to render
- # email from a faulty Django template
- try:
- email.prepare_email_message()
- except Exception as e:
- failed_emails.append((email, e))
-
- number_of_threads = min(get_threads_per_process(), email_count)
- pool = ThreadPool(number_of_threads)
-
- pool.map(send, emails)
- pool.close()
- pool.join()
-
- connections.close()
-
- # Update statuses of sent and failed emails
- email_ids = [email.id for email in sent_emails]
- Email.objects.filter(id__in=email_ids).update(status=STATUS.sent)
-
- email_ids = [email.id for (email, e) in failed_emails]
- Email.objects.filter(id__in=email_ids).update(status=STATUS.failed)
-
- # If log level is 0, log nothing, 1 logs only sending failures
- # and 2 means log both successes and failures
- if log_level >= 1:
-
- logs = []
- for (email, exception) in failed_emails:
- logs.append(
- Log(email=email, status=STATUS.failed,
- message=str(exception),
- exception_type=type(exception).__name__)
- )
-
- if logs:
- Log.objects.bulk_create(logs)
-
- if log_level == 2:
-
- logs = []
- for email in sent_emails:
- logs.append(Log(email=email, status=STATUS.sent))
-
- if logs:
- Log.objects.bulk_create(logs)
-
- logger.info(
- 'Process finished, %s attempted, %s sent, %s failed' % (
- email_count, len(sent_emails), len(failed_emails)
- )
- )
-
- return len(sent_emails), len(failed_emails)
|