]> git.ipfire.org Git - ipfire.org.git/commitdiff
accounts: Send a probe message after a few bounces
authorMichael Tremer <michael.tremer@ipfire.org>
Tue, 20 Jan 2026 17:42:16 +0000 (17:42 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Tue, 20 Jan 2026 17:42:16 +0000 (17:42 +0000)
Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Makefile.am
src/backend/accounts.py
src/backend/base.py
src/backend/messages.py
src/templates/accounts/messages/probe.txt [new file with mode: 0644]

index 5a523aa89be5b7c858e6e91d361e3854d3618068..7d2d5eaa36d6e72f5e3d2b75095213b76f1f9834 100644 (file)
@@ -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
index ae248a1d9609ccf791b9e9a227ca9101df5a8600..ebe6fb4f5db95c29d6f6eea11683bbce6efe9901 100644 (file)
@@ -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 = (
index 1d86906d912e558aab90af03a6dc487e13d4b771..cc3bc7b245ab6c81bc7607746d24e79d4e43e68c 100644 (file)
@@ -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,
index fd3ec382408bd16f8b20c651b68c03d02b394743..ea1eaf18104f041b71ae0db46ad0d74f7ad4c5f1 100644 (file)
@@ -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 (file)
index 0000000..fe2e580
--- /dev/null
@@ -0,0 +1,25 @@
+From: IPFire Project <no-reply@ipfire.org>
+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