From: Michael Tremer Date: Tue, 20 Jan 2026 17:42:16 +0000 (+0000) Subject: accounts: Send a probe message after a few bounces X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=695e40ee55662f08345073e2b675222f241decd2;p=ipfire.org.git accounts: Send a probe message after a few bounces Signed-off-by: Michael Tremer --- diff --git a/Makefile.am b/Makefile.am index 5a523aa8..7d2d5eaa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -113,6 +113,13 @@ templates_DATA = \ templatesdir = $(datadir)/templates +templates_accountsdir = $(templatesdir)/accounts + +templates_accounts_messages_DATA = \ + src/templates/accounts/messages/probe.txt + +templates_accounts_messagesdir = $(templates_accountsdir)/messages + templates_analytics_DATA = \ src/templates/analytics/docs.html \ src/templates/analytics/index.html diff --git a/src/backend/accounts.py b/src/backend/accounts.py index ae248a1d..ebe6fb4f 100644 --- a/src/backend/accounts.py +++ b/src/backend/accounts.py @@ -672,6 +672,21 @@ class Accounts(Object): with self.db.transaction(): await account.delete(who) + async def _probe(self, *args): + """ + Generates a probe email to the given users + """ + for arg in args: + account = self.find_account(arg) + + # Skip if we could not find an account + if not account: + print("Could not find account %s" % arg) + continue + + # Send the probe + account.send_probe() + # Discourse def decode_discourse_payload(self, payload, signature): @@ -1643,6 +1658,12 @@ class Account(LDAPObject): # Log the total score log.info("%s (%s) has a total bounce score of %s" % (self, self.uid, total_score)) + # If the score is above 5, we will try to send a probe + if total_score >= 5: + # Delay the probe because if we have just received a bounce, it is unlikely + # that the condition that has caused this has already been resolved. + self.send_probe(delay=datetime.timedelta(hours=6)) + def _determine_bounce_score(self, msg): """ Returns the bounce score from the message @@ -1713,6 +1734,61 @@ class Account(LDAPObject): # Return the score or zero return res.score if res else 0 + def send_probe(self, force=False, delay=None, **kwargs): + """ + Very likely will send a probe message to the account email address + """ + now = datetime.datetime.now() + + if not force: + # Fetch the time the last probe was sent + last_probe_sent_at = self.get_last_probe_sent_at() + + # If we have a time, let's not send another probe too soon + if last_probe_sent_at: + delta = now - last_probe_sent_at + + # Only send a probe once a week + if delta < datetime.timedelta(days=7): + log.debug("Won't send probe to %s because we just sent one at %s" \ + % (self, last_probe_sent_at)) + return + + # Send the message + t = self.send_message("accounts/messages/probe", delay=delay) + + # Store the time we have sent a probe + self.db.execute(""" + INSERT INTO + account_probes + ( + uid, sent_at + ) + VALUES + ( + %s, %s + ) + """, self.uid, t, + ) + + def get_last_probe_sent_at(self): + """ + Returns the timestamp of the last probe being sent + """ + res = self.db.get(""" + SELECT + sent_at + FROM + account_probes + WHERE + uid = %s + ORDER BY + sent_at DESC + LIMIT 1 + """, self.uid, + ) + + return res.sent_at if res else None class Groups(Object): hidden_groups = ( diff --git a/src/backend/base.py b/src/backend/base.py index 1d86906d..cc3bc7b2 100644 --- a/src/backend/base.py +++ b/src/backend/base.py @@ -159,6 +159,7 @@ class Backend(object): async def run_task(self, task, *args, **kwargs): tasks = { "accounts:delete" : self.accounts._delete, + "accounts:probe" : self.accounts._probe, "announce-blog-posts" : self.blog.announce, "campaigns:donate" : self.campaigns.donate, "campaigns:send" : self.campaigns.send, diff --git a/src/backend/messages.py b/src/backend/messages.py index fd3ec382..ea1eaf18 100644 --- a/src/backend/messages.py +++ b/src/backend/messages.py @@ -105,6 +105,9 @@ class Messages(misc.Object): logging.debug("Message queued with ID %s" % res.id) + # Return the timestamp after this email will be sent + return after + def send_template(self, template_name, sender=None, priority=0, headers={}, after=None, **kwargs): """ @@ -176,11 +179,13 @@ class Messages(misc.Object): message.attach(message_part) # Send the message - self.send(message, priority=priority, headers=headers, after=after) + try: + return self.send(message, priority=priority, headers=headers, after=after) # In debug mode, re-compile the templates with every request - if self.backend.debug: - self.template_loader.reset() + finally: + if self.backend.debug: + self.template_loader.reset() def _inline_css(self, part): """ diff --git a/src/templates/accounts/messages/probe.txt b/src/templates/accounts/messages/probe.txt new file mode 100644 index 00000000..fe2e580a --- /dev/null +++ b/src/templates/accounts/messages/probe.txt @@ -0,0 +1,25 @@ +From: IPFire Project +To: {{ account.email_to }} +Subject: {{ _("Hey, are you still there?") }} + +Hi {{ account.first_name }}, + +We've tried sending you a few notifications from IPFire, but they +keep bouncing back. + +Did you change your email address? Is your inbox full? Either way, we want +to make sure you're still getting our updates if you want them! + +Click here to confirm you still want to hear from us: + + [VERIFY_LINK] + +Or update your email address in your settings: + + [SETTINGS_LINK] + +No worries if you'd rather not receive emails - just ignore this message +and we'll stop trying and close your account. + +Cheers, +-Your IPFire.org Team