Repository: liberapay/liberapay.com

Issue:
Donation renewal reminders aren't sent to users lacking a verified email address
This wouldn't be a problem if users confirmed their email addresses, but a lot of them don't: as of now we have 6054 active accounts with no verified email address, versus 8402 with at least one verified email. Once a user has missed the original verification email we don't do anything to help them rectify the problem.

A quick DB query suggests that we currently have 66 donors who need to renew their donations but haven't been notified by email. (SQL: `select count(*) from participants p where p.status = 'active' and p.email is null and p.is_suspended is not true and exists (select 1 from notifications n where n.participant = p.id and n.event = 'donate_reminder' and n.email_sent is false);`)

This issue is related to #61, because reducing the risk of spam is the reason why we don't send emails when the address isn't verified.


Discussion:
> Once a user has missed the original verification email we don't do anything to help them rectify the problem.

If #61 is fixed (enabling a blacklist), I see no reason to not send maybe three separate emails asking for verification. This would be on par with other websites in my experience.

When an account it set up, is it very obvious that if you don't verify your email address, you won't receive emails, including notifications to renew donations? Could the email verification requirement be built in earlier?

Resolving patch:
diff --git a/liberapay/billing/payday.py b/liberapay/billing/payday.py
--- a/liberapay/billing/payday.py
+++ b/liberapay/billing/payday.py
@@ -1090,17 +1090,17 @@ def notify_participants(self):
                        SELECT t.periodic_amount, t.tippee_username
                    ) a))
               FROM (
-                     SELECT t.*, p2.username AS tippee_username
+                     SELECT t.*, tippee_p.username AS tippee_username
                        FROM current_tips t
-                       JOIN participants p2 ON p2.id = t.tippee
+                       JOIN participants tippee_p ON tippee_p.id = t.tippee
                       WHERE t.renewal_mode > 0
                         AND ( t.paid_in_advance IS NULL OR
                               t.paid_in_advance < t.amount
                             )
-                        AND p2.status = 'active'
-                        AND p2.is_suspended IS NOT true
-                        AND p2.payment_providers > 0
-                        AND (p2.goal IS NULL OR p2.goal >= 0)
+                        AND tippee_p.status = 'active'
+                        AND tippee_p.is_suspended IS NOT true
+                        AND tippee_p.payment_providers > 0
+                        AND ( tippee_p.goal IS NULL OR tippee_p.goal >= 0 )
                    ) t
              WHERE EXISTS (
                      SELECT 1
@@ -1109,15 +1109,16 @@ def notify_participants(self):
                         AND COALESCE(tr.team, tr.tippee) = t.tippee
                         AND tr.context IN ('tip', 'take')
                         AND tr.status = 'succeeded'
-                        AND tr.timestamp >= (current_timestamp - interval '9 weeks')
+                        AND tr.timestamp >= (current_date - interval '26 weeks')
                    )
-               AND (
-                     SELECT count(*)
+               AND NOT EXISTS (
+                     SELECT 1
                        FROM notifications n
                       WHERE n.participant = t.tipper
                         AND n.event = 'donate_reminder'
+                        AND n.ts >= (current_date - interval '3 weeks')
                         AND n.is_new
-                   ) < 2
+                   )
                AND NOT EXISTS (
                      SELECT 1
                        FROM payin_transfers pt
@@ -1125,15 +1126,17 @@ def notify_participants(self):
                         AND COALESCE(pt.team, pt.recipient) = t.tippee
                         AND pt.context IN ('personal-donation', 'team-donation')
                         AND pt.status = 'pending'
-                        AND pt.ctime >= (current_timestamp - interval '9 weeks')
+                        AND pt.ctime >= (current_date - interval '3 weeks')
                    )
           GROUP BY t.tipper
           ORDER BY t.tipper
         """)
         for p, donations in participants:
+            if p.status != 'active' or p.is_suspended:
+                continue
             for tip in donations:
                 tip['periodic_amount'] = Money(**tip['periodic_amount'])
-            p.notify('donate_reminder', donations=donations)
+            p.notify('donate_reminder', donations=donations, email_unverified_address=True)
             n += 1
         log("Sent %i donate_reminder notifications." % n)
 
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -1265,13 +1265,17 @@ def set_email_lang(self, lang, cursor=None):
     # Notifications
     # =============
 
-    def notify(self, event, force_email=False, email=True, web=True, idem_key=None, **context):
+    def notify(self, event, force_email=False, email=True, web=True, idem_key=None,
+               email_unverified_address=False, **context):
         if email and not force_email:
             bit = EVENTS.get(event.split('~', 1)[0]).bit
             email = self.email_notif_bits & bit > 0
         p_id = self.id
-        context = serialize(context)
+        # If email_unverified_address is on, allow sending to an unverified email address.
+        if email_unverified_address and not self.email:
+            context['email'] = self.get_email_address()
         # Check that this notification isn't a duplicate
+        context = serialize(context)
         n = self.db.one("""
             SELECT count(*)
               FROM notifications


Test patch: