Repository: liberapay/liberapay.com

Issue:
Confirmation e-mail isn't generated correctly.
I've just opened an account and can't validate or add any connections neither Paypal nor Stripe because generated email doesn't contain correct slug.

<img width="834" alt="Screenshot 2019-07-27 at 21 07 25" src="https://user-images.githubusercontent.com/578559/61998593-93b09000-b0b2-11e9-9590-3ac840db70d7.png">



Discussion:
Confirmed. It's a regression from #1530. It only affects the initial email sent during account creation.

Resolving patch:
diff --git a/liberapay/models/participant.py b/liberapay/models/participant.py
--- a/liberapay/models/participant.py
+++ b/liberapay/models/participant.py
@@ -1015,10 +1015,11 @@ def add_email(self, email, cursor=None):
             # Log event
             self.add_event(c, 'add_email', email)
 
-        self.send_email('verification', email, old_email=old_email)
+        self.send_email('verification', email_row, old_email=old_email)
 
         if self.email:
-            self.send_email('verification_notice', self.email, new_email=email)
+            primary_email_row = self.get_email(self.email, cursor=cursor)
+            self.send_email('verification_notice', primary_email_row, new_email=email)
             return 2
         else:
             self.update_avatar(cursor=cursor)
@@ -1095,8 +1096,8 @@ def verify_email(self, email_id, nonce, user):
             self.update_email(r.address)
         return EmailVerificationResult.SUCCEEDED
 
-    def get_email(self, email):
-        return self.db.one("""
+    def get_email(self, email, cursor=None):
+        return (cursor or self.db).one("""
             SELECT *
               FROM emails
              WHERE participant=%s
@@ -1151,10 +1152,10 @@ def remove_email(self, address):
             if n_left == 0:
                 raise CannotRemovePrimaryEmail()
 
-    def send_email(self, spt_name, email, **context):
+    def send_email(self, spt_name, email_row, **context):
+        email = email_row.address
         check_email_blacklist(email)
-        email_row = self.get_email(email)
-        if email_row and email_row.disavowed:
+        if email_row.disavowed:
             raise EmailAddressIsBlacklisted(email, 'complaint', email_row.disavowed_time)
         self.fill_notification_context(context)
         context['email'] = email
@@ -1190,7 +1191,7 @@ def render(t, context):
             message['from_email'] = 'Liberapay Newsletters <newsletters@liberapay.com>'
         message['to'] = [formataddr((self.username, email))]
         message['subject'] = spt['subject'].render(context).strip()
-        self._rendering_email_to, self._email_session = email, None
+        self._rendering_email_to, self._email_session = email_row, None
         message['html'] = render('text/html', context_html)
         message['text'] = render('text/plain', context)
         del self._rendering_email_to, self._email_session
@@ -1234,8 +1235,9 @@ def _dequeue_emails(cls, cursor):
                 if not email:
                     dequeue(msg, False)
                     continue
+                email_row = p.get_email(email)
                 try:
-                    p.send_email(msg.event, email, **d)
+                    p.send_email(msg.event, email_row, **d)
                 except EmailAddressIsBlacklisted:
                     dequeue(msg, False)
                 except Exception as e:
@@ -1657,13 +1659,14 @@ def url(self, path='', query='', autologin=False):
         username = self.username
         if query:
             query = '?' + urlencode(query)
-        if getattr(self, '_rendering_email_to', None):
+        email_row = getattr(self, '_rendering_email_to', None)
+        if email_row:
             extra_query = []
             if autologin:
                 primary_email = self.get_email_address()
-                if self._rendering_email_to.lower() != primary_email.lower():
+                if email_row.address.lower() != primary_email.lower():
                     # Only send login links to the primary email address
-                    raise AssertionError('%r != %r' % (self._rendering_email_to, primary_email))
+                    raise AssertionError('%r != %r' % (email_row.address, primary_email))
                 session = self._email_session
                 if not session:
                     session = self.start_session(suffix='.em', id_min=1001, id_max=1010)
@@ -1671,14 +1674,7 @@ def url(self, path='', query='', autologin=False):
                 extra_query.append(('log-in.id', self.id))
                 extra_query.append(('log-in.key', session.id))
                 extra_query.append(('log-in.token', session.secret))
-            email_row = self.db.one("""
-                SELECT e.*
-                  FROM emails e
-                 WHERE lower(e.address) = lower(%s)
-                   AND e.participant = %s
-                   AND e.verified IS NULL
-            """, (self._rendering_email_to, self.id))
-            if email_row:
+            if not email_row.verified:
                 extra_query.append(('email.id', email_row.id))
                 extra_query.append(('email.nonce', email_row.nonce))
             if extra_query:
diff --git a/liberapay/security/authentication.py b/liberapay/security/authentication.py
--- a/liberapay/security/authentication.py
+++ b/liberapay/security/authentication.py
@@ -87,7 +87,8 @@ def sign_in_with_form_data(body, state):
                 if not p.get_email(email).verified:
                     website.db.hit_rate_limit('log-in.email.not-verified', email, TooManyLoginEmails)
                 website.db.hit_rate_limit('log-in.email', p.id, TooManyLoginEmails)
-                p.send_email('login_link', email, link_validity=SESSION_TIMEOUT)
+                email_row = p.get_email(email)
+                p.send_email('login_link', email_row, link_validity=SESSION_TIMEOUT)
                 state['log-in.email-sent-to'] = email
                 raise LoginRequired
             else:


Test patch:
diff --git a/tests/py/test_email.py b/tests/py/test_email.py
--- a/tests/py/test_email.py
+++ b/tests/py/test_email.py
@@ -216,14 +216,19 @@ def test_verified_email_is_not_changed_after_update(self):
         assert expected == actual
 
     def test_disavow_email(self):
-        self.alice.add_email('alice@liberapay.com')
-        email = self.alice.get_email('alice@liberapay.com')
-        url = '/alice/emails/disavow?email.id=%s&email.nonce=%s' % (email.id, email.nonce)
+        self.client.PxST('/sign-up', {
+            'sign-in.email': 'bob@liberapay.com',
+            'sign-in.username': 'bob',
+            'sign-in.currency': 'USD',
+        })
+        bob = Participant.from_username('bob')
+        email = bob.get_email('bob@liberapay.com')
+        url = '/bob/emails/disavow?email.id=%s&email.nonce=%s' % (email.id, email.nonce)
         verification_email = self.get_last_email()
         assert url in verification_email['text']
         r = self.client.GET(url)
         assert r.code == 200
-        email = self.alice.get_email(email.address)
+        email = bob.get_email(email.address)
         assert email.disavowed is True
         assert email.disavowed_time is not None
         assert email.verified is None
@@ -235,7 +240,11 @@ def test_disavow_email(self):
         assert r.code == 200
 
         # Check that resending the verification email isn't allowed
-        self.hit_email_spt('resend', email.address, expected_code=400)
+        r = self.client.POST(
+            '/bob/emails/modify.json', {'resend': email.address},
+            auth_as=bob, raise_immediately=False,
+        )
+        assert r.code == 400, r.text
 
         # Test adding the address to the blacklist
         r = self.client.POST(url, {'action': 'add_to_blacklist'})