Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mail.py 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. from multiprocessing import Pool
  2. from multiprocessing.dummy import Pool as ThreadPool
  3. from django.conf import settings
  4. from django.core.exceptions import ValidationError
  5. from django.db import connection as db_connection
  6. from django.db.models import Q
  7. from django.template import Context, Template
  8. from django.utils.timezone import now
  9. from .connections import connections
  10. from .models import Email, EmailTemplate, Log, PRIORITY, STATUS
  11. from .settings import (get_available_backends, get_batch_size,
  12. get_log_level, get_sending_order, get_threads_per_process)
  13. from .utils import (get_email_template, parse_emails, parse_priority,
  14. split_emails, create_attachments)
  15. from .logutils import setup_loghandlers
  16. logger = setup_loghandlers("INFO")
  17. def create(sender, recipients=None, cc=None, bcc=None, subject='', message='',
  18. html_message='', context=None, scheduled_time=None, headers=None,
  19. template=None, priority=None, render_on_delivery=False, commit=True,
  20. backend=''):
  21. """
  22. Creates an email from supplied keyword arguments. If template is
  23. specified, email subject and content will be rendered during delivery.
  24. """
  25. priority = parse_priority(priority)
  26. status = None if priority == PRIORITY.now else STATUS.queued
  27. if recipients is None:
  28. recipients = []
  29. if cc is None:
  30. cc = []
  31. if bcc is None:
  32. bcc = []
  33. if context is None:
  34. context = ''
  35. # If email is to be rendered during delivery, save all necessary
  36. # information
  37. if render_on_delivery:
  38. email = Email(
  39. from_email=sender,
  40. to=recipients,
  41. cc=cc,
  42. bcc=bcc,
  43. scheduled_time=scheduled_time,
  44. headers=headers, priority=priority, status=status,
  45. context=context, template=template, backend_alias=backend
  46. )
  47. else:
  48. if template:
  49. subject = template.subject
  50. message = template.content
  51. html_message = template.html_content
  52. _context = Context(context or {})
  53. subject = Template(subject).render(_context)
  54. message = Template(message).render(_context)
  55. html_message = Template(html_message).render(_context)
  56. email = Email(
  57. from_email=sender,
  58. to=recipients,
  59. cc=cc,
  60. bcc=bcc,
  61. subject=subject,
  62. message=message,
  63. html_message=html_message,
  64. scheduled_time=scheduled_time,
  65. headers=headers, priority=priority, status=status,
  66. backend_alias=backend
  67. )
  68. if commit:
  69. email.save()
  70. return email
  71. def send(recipients=None, sender=None, template=None, context=None, subject='',
  72. message='', html_message='', scheduled_time=None, headers=None,
  73. priority=None, attachments=None, render_on_delivery=False,
  74. log_level=None, commit=True, cc=None, bcc=None, language='',
  75. backend=''):
  76. try:
  77. recipients = parse_emails(recipients)
  78. except ValidationError as e:
  79. raise ValidationError('recipients: %s' % e.message)
  80. try:
  81. cc = parse_emails(cc)
  82. except ValidationError as e:
  83. raise ValidationError('c: %s' % e.message)
  84. try:
  85. bcc = parse_emails(bcc)
  86. except ValidationError as e:
  87. raise ValidationError('bcc: %s' % e.message)
  88. if sender is None:
  89. sender = settings.DEFAULT_FROM_EMAIL
  90. priority = parse_priority(priority)
  91. if log_level is None:
  92. log_level = get_log_level()
  93. if not commit:
  94. if priority == PRIORITY.now:
  95. raise ValueError("send_many() can't be used with priority = 'now'")
  96. if attachments:
  97. raise ValueError("Can't add attachments with send_many()")
  98. if template:
  99. if subject:
  100. raise ValueError('You can\'t specify both "template" and "subject" arguments')
  101. if message:
  102. raise ValueError('You can\'t specify both "template" and "message" arguments')
  103. if html_message:
  104. raise ValueError('You can\'t specify both "template" and "html_message" arguments')
  105. # template can be an EmailTemplate instance or name
  106. if isinstance(template, EmailTemplate):
  107. template = template
  108. # If language is specified, ensure template uses the right language
  109. if language:
  110. if template.language != language:
  111. template = template.translated_templates.get(language=language)
  112. else:
  113. template = get_email_template(template, language)
  114. if backend and backend not in get_available_backends().keys():
  115. raise ValueError('%s is not a valid backend alias' % backend)
  116. email = create(sender, recipients, cc, bcc, subject, message, html_message,
  117. context, scheduled_time, headers, template, priority,
  118. render_on_delivery, commit=commit, backend=backend)
  119. if attachments:
  120. attachments = create_attachments(attachments)
  121. email.attachments.add(*attachments)
  122. if priority == PRIORITY.now:
  123. email.dispatch(log_level=log_level)
  124. return email
  125. def send_many(kwargs_list):
  126. """
  127. Similar to mail.send(), but this function accepts a list of kwargs.
  128. Internally, it uses Django's bulk_create command for efficiency reasons.
  129. Currently send_many() can't be used to send emails with priority = 'now'.
  130. """
  131. emails = []
  132. for kwargs in kwargs_list:
  133. emails.append(send(commit=False, **kwargs))
  134. Email.objects.bulk_create(emails)
  135. def get_queued():
  136. """
  137. Returns a list of emails that should be sent:
  138. - Status is queued
  139. - Has scheduled_time lower than the current time or None
  140. """
  141. return Email.objects.filter(status=STATUS.queued) \
  142. .select_related('template') \
  143. .filter(Q(scheduled_time__lte=now()) | Q(scheduled_time=None)) \
  144. .order_by(*get_sending_order()).prefetch_related('attachments')[:get_batch_size()]
  145. def send_queued(processes=1, log_level=None):
  146. """
  147. Sends out all queued mails that has scheduled_time less than now or None
  148. """
  149. queued_emails = get_queued()
  150. total_sent, total_failed = 0, 0
  151. total_email = len(queued_emails)
  152. logger.info('Started sending %s emails with %s processes.' %
  153. (total_email, processes))
  154. if log_level is None:
  155. log_level = get_log_level()
  156. if queued_emails:
  157. # Don't use more processes than number of emails
  158. if total_email < processes:
  159. processes = total_email
  160. if processes == 1:
  161. total_sent, total_failed = _send_bulk(queued_emails,
  162. uses_multiprocessing=False,
  163. log_level=log_level)
  164. else:
  165. email_lists = split_emails(queued_emails, processes)
  166. pool = Pool(processes)
  167. results = pool.map(_send_bulk, email_lists)
  168. pool.terminate()
  169. total_sent = sum([result[0] for result in results])
  170. total_failed = sum([result[1] for result in results])
  171. message = '%s emails attempted, %s sent, %s failed' % (
  172. total_email,
  173. total_sent,
  174. total_failed
  175. )
  176. logger.info(message)
  177. return (total_sent, total_failed)
  178. def _send_bulk(emails, uses_multiprocessing=True, log_level=None):
  179. # Multiprocessing does not play well with database connection
  180. # Fix: Close connections on forking process
  181. # https://groups.google.com/forum/#!topic/django-users/eCAIY9DAfG0
  182. if uses_multiprocessing:
  183. db_connection.close()
  184. if log_level is None:
  185. log_level = get_log_level()
  186. sent_emails = []
  187. failed_emails = [] # This is a list of two tuples (email, exception)
  188. email_count = len(emails)
  189. logger.info('Process started, sending %s emails' % email_count)
  190. def send(email):
  191. try:
  192. email.dispatch(log_level=log_level, commit=False,
  193. disconnect_after_delivery=False)
  194. sent_emails.append(email)
  195. logger.debug('Successfully sent email #%d' % email.id)
  196. except Exception as e:
  197. logger.debug('Failed to send email #%d' % email.id)
  198. failed_emails.append((email, e))
  199. # Prepare emails before we send these to threads for sending
  200. # So we don't need to access the DB from within threads
  201. for email in emails:
  202. # Sometimes this can fail, for example when trying to render
  203. # email from a faulty Django template
  204. try:
  205. email.prepare_email_message()
  206. except Exception as e:
  207. failed_emails.append((email, e))
  208. number_of_threads = min(get_threads_per_process(), email_count)
  209. pool = ThreadPool(number_of_threads)
  210. pool.map(send, emails)
  211. pool.close()
  212. pool.join()
  213. connections.close()
  214. # Update statuses of sent and failed emails
  215. email_ids = [email.id for email in sent_emails]
  216. Email.objects.filter(id__in=email_ids).update(status=STATUS.sent)
  217. email_ids = [email.id for (email, e) in failed_emails]
  218. Email.objects.filter(id__in=email_ids).update(status=STATUS.failed)
  219. # If log level is 0, log nothing, 1 logs only sending failures
  220. # and 2 means log both successes and failures
  221. if log_level >= 1:
  222. logs = []
  223. for (email, exception) in failed_emails:
  224. logs.append(
  225. Log(email=email, status=STATUS.failed,
  226. message=str(exception),
  227. exception_type=type(exception).__name__)
  228. )
  229. if logs:
  230. Log.objects.bulk_create(logs)
  231. if log_level == 2:
  232. logs = []
  233. for email in sent_emails:
  234. logs.append(Log(email=email, status=STATUS.sent))
  235. if logs:
  236. Log.objects.bulk_create(logs)
  237. logger.info(
  238. 'Process finished, %s attempted, %s sent, %s failed' % (
  239. email_count, len(sent_emails), len(failed_emails)
  240. )
  241. )
  242. return len(sent_emails), len(failed_emails)