from datetime import timedelta from django.conf import settings from django.core import mail from django.core.management.base import BaseCommand from django.utils import timezone from main import models, util def get_mail_connection(): if settings.DEBUG: return mail.get_connection("django.core.mail.backends.console.EmailBackend") # SMPT EmailBackend instantiated with the broadcast-specific email host return mail.get_connection( "django.core.mail.backends.smtp.EmailBackend", host=settings.EMAIL_HOST_BROADCASTS, ) def get_email_body(post, notification): """Returns the email body (which contains the post body) along with titles and links.""" post_url = util.get_protocol() + post.get_proper_url() unsubscribe_url = util.get_protocol() + notification.get_unsubscribe_url() blog_title = post.owner.blog_title or post.owner.username body = f"""{blog_title} has published: # {post.title} {post_url} {post.body} --- Blog post URL: {post_url} --- Unsubscribe: {unsubscribe_url} """ return body def get_email(post, notification): """Returns the email object, containing all info needed to be sent.""" blog_title = post.owner.username # email sender name cannot contain commas if post.owner.blog_title and "," not in post.owner.blog_title: blog_title = post.owner.blog_title unsubscribe_url = util.get_protocol() + notification.get_unsubscribe_url() body = get_email_body(post, notification) email = mail.EmailMessage( subject=post.title, body=body, from_email=f"{blog_title} <{post.owner.username}@{settings.EMAIL_FROM_HOST}>", to=[notification.email], headers={ "X-PM-Message-Stream": "newsletters", "List-Unsubscribe": unsubscribe_url, "List-Unsubscribe-Post": "List-Unsubscribe=One-Click", }, ) return email class Command(BaseCommand): help = "Process new posts and send email to subscribers" def add_arguments(self, parser): parser.add_argument( "--no-dryrun", action="store_false", dest="dryrun", help="No dry run. Send actual emails.", ) parser.set_defaults(dryrun=True) def handle(self, *args, **options): self.stdout.write(self.style.NOTICE("Processing notifications.")) yesterday = timezone.now().date() - timedelta(days=1) post_list = models.Post.objects.filter( owner__notifications_on=True, broadcasted_at__isnull=True, published_at=yesterday, ) self.stdout.write(self.style.NOTICE(f"Post count to process: {len(post_list)}")) count_sent = 0 connection = get_mail_connection() # for all posts that were published yesterday for post in post_list: # assume no notification will fail no_send_failures = True notification_list = models.Notification.objects.filter( blog_user=post.owner, is_active=True, ) msg = ( f"Subscriber count for: '{post.title}' (author: {post.owner.username})" f" is {len(notification_list)}." ) self.stdout.write(self.style.NOTICE(msg)) # for every email address subcribed to the post's blog owner for notification in notification_list: # don't send if dry run mode if options["dryrun"]: msg = f"Would otherwise sent: '{post.title}' for '{notification.email}'." self.stdout.write(self.style.NOTICE(msg)) continue # log record record, created = models.NotificationRecord.objects.get_or_create( notification=notification, post=post, ) # check if this post id has already been sent to this email # could be because the published_at date has been changed if created: # keep count of all emails of this run count_sent += 1 # sent out email email = get_email(post, notification) try: connection.send_messages([email]) except Exception as ex: no_send_failures = False msg = f"Failed to send '{post.title}' to {notification.email}." self.stdout.write(self.style.ERROR(msg)) record.delete() self.stdout.write(self.style.ERROR(ex)) msg = f"Email sent for '{post.title}' to '{notification.email}'." self.stdout.write(self.style.SUCCESS(msg)) else: msg = ( f"No email sent for '{post.title}' to '{notification.email}'. " f"Email was sent {record.sent_at}" ) self.stdout.write(self.style.NOTICE(msg)) # broadcast for this post done if not options["dryrun"] and no_send_failures: post.broadcasted_at = timezone.now() post.save() # broadcast for all posts done connection.close() # return if send mode is off if options["dryrun"]: self.stdout.write( self.style.SUCCESS("Broadcast dry run done. No emails were sent.") ) return self.stdout.write( self.style.SUCCESS(f"Broadcast sent. Total {count_sent} emails.") )