import logging import time from smtplib import SMTPException from django.conf import settings from django.core.mail import send_mail from django.core.management.base import BaseCommand from django.db.models import Exists, OuterRef, QuerySet, Subquery from django.template.loader import render_to_string from django.utils.timezone import localdate, now from django.utils.translation import gettext as _ from core.models import User from counter.models import AccountDump from subscription.models import Subscription class Command(BaseCommand): """Send mail to inactive users, warning them that their account is about to be dumped. This command should be automated with a cron task. """ def __init__(self, *args, **kwargs): self.logger = logging.getLogger("account_dump_mail") self.logger.setLevel(logging.INFO) super().__init__(*args, **kwargs) def add_arguments(self, parser): parser.add_argument( "--dry-run", action="store_true", help="Do not send the mails, just display the number of users concerned", ) parser.add_argument( "-d", "--delay", type=float, default=0, help="Delay in seconds between each mail sent", ) def handle(self, *args, **options): users: list[User] = list(self._get_users()) self.stdout.write(f"{len(users)} users will be warned of their account dump") if options["verbosity"] > 1: self.stdout.write("Users concerned:\n") self.stdout.write( "\n".join( f" - {user.get_display_name()} ({user.email}) : " f"{user.customer.amount} €" for user in users ) ) if options["dry_run"]: return dumps = [] for user in users: is_success = self._send_mail(user) dumps.append( AccountDump( customer_id=user.id, warning_mail_sent_at=now(), warning_mail_error=not is_success, ) ) if options["delay"]: # avoid spamming the mail server too much time.sleep(options["delay"]) AccountDump.objects.bulk_create(dumps) self.stdout.write("Finished !") @staticmethod def _get_users() -> QuerySet[User]: ongoing_dump_operation = AccountDump.objects.ongoing().filter( customer__user=OuterRef("pk") ) return ( User.objects.filter_inactive() .filter(customer__amount__gt=0) .exclude(Exists(ongoing_dump_operation)) .annotate( last_subscription_date=Subquery( Subscription.objects.filter(member=OuterRef("pk")) .order_by("-subscription_end") .values("subscription_end")[:1] ), ) .select_related("customer") ) def _send_mail(self, user: User) -> bool: """Send the warning email to the given user. Returns: True if the mail was successfully sent, else False """ message = render_to_string( "counter/account_dump_warning_mail.jinja", { "balance": user.customer.amount, "last_subscription_date": user.last_subscription_date, "dump_date": localdate() + settings.SITH_ACCOUNT_DUMP_DELTA, }, ) try: # sending mails one by one is long and ineffective, # but it makes easier to know which emails failed (and how). # Also, there won't be that much mails sent (except on the first run) send_mail( _("Clearing of your AE account"), message, settings.DEFAULT_FROM_EMAIL, [user.email], ) self.logger.info(f"Mail successfully sent to {user.email}") return True except SMTPException as e: self.logger.error(f"failed mail to {user.email} :\n{e}") return False