14
14
# limitations under the License.
15
15
16
16
import logging
17
+ import os
17
18
import time
18
19
from typing import Optional , Tuple
19
20
24
25
25
26
from email_account_validity ._config import EmailAccountValidityConfig
26
27
from email_account_validity ._store import EmailAccountValidityStore
27
- from email_account_validity ._utils import random_string
28
+ from email_account_validity ._utils import (
29
+ LONG_TOKEN_REGEX ,
30
+ SHORT_TOKEN_REGEX ,
31
+ random_digit_string ,
32
+ random_string ,
33
+ TokenFormat ,
34
+ )
28
35
29
36
logger = logging .getLogger (__name__ )
30
37
@@ -40,9 +47,11 @@ def __init__(
40
47
self ._store = store
41
48
42
49
self ._period = config .period
50
+ self ._send_links = config .send_links
43
51
44
52
(self ._template_html , self ._template_text ,) = api .read_templates (
45
53
["notice_expiry.html" , "notice_expiry.txt" ],
54
+ os .path .join (os .path .dirname (os .path .abspath (__file__ )), "templates" ),
46
55
)
47
56
48
57
if config .renew_email_subject is not None :
@@ -53,7 +62,7 @@ def __init__(
53
62
try :
54
63
app_name = self ._api .email_app_name
55
64
self ._renew_email_subject = renew_email_subject % {"app" : app_name }
56
- except Exception :
65
+ except ( KeyError , TypeError ) :
57
66
# If substitution failed, fall back to the bare strings.
58
67
self ._renew_email_subject = renew_email_subject
59
68
@@ -110,35 +119,68 @@ async def send_renewal_email(self, user_id: str, expiration_ts: int):
110
119
except SynapseError :
111
120
display_name = user_id
112
121
113
- renewal_token = await self .generate_renewal_token (user_id )
122
+ # If the user isn't expected to click on a link, but instead to copy the token
123
+ # into their client, we generate a different kind of token, simpler and shorter,
124
+ # because a) we don't need it to be unique to the whole table and b) we want the
125
+ # user to be able to be easily type it back into their client.
126
+ if self ._send_links :
127
+ renewal_token = await self .generate_unauthenticated_renewal_token (user_id )
114
128
115
- url = "%s_synapse/client/email_account_validity/renew?token=%s" % (
116
- self ._api .public_baseurl ,
117
- renewal_token ,
118
- )
129
+ url = "%s_synapse/client/email_account_validity/renew?token=%s" % (
130
+ self ._api .public_baseurl ,
131
+ renewal_token ,
132
+ )
133
+ else :
134
+ renewal_token = await self .generate_authenticated_renewal_token (user_id )
135
+ url = None
119
136
120
137
template_vars = {
138
+ "app_name" : self ._api .email_app_name ,
121
139
"display_name" : display_name ,
122
140
"expiration_ts" : expiration_ts ,
123
141
"url" : url ,
142
+ "renewal_token" : renewal_token ,
124
143
}
125
144
126
145
html_text = self ._template_html .render (** template_vars )
127
146
plain_text = self ._template_text .render (** template_vars )
128
147
129
148
for address in addresses :
130
149
await self ._api .send_mail (
131
- address ,
132
- self ._renew_email_subject ,
133
- html_text ,
134
- plain_text ,
150
+ recipient = address ,
151
+ subject = self ._renew_email_subject ,
152
+ html = html_text ,
153
+ text = plain_text ,
135
154
)
136
155
137
156
await self ._store .set_renewal_mail_status (user_id = user_id , email_sent = True )
138
157
139
- async def generate_renewal_token (self , user_id : str ) -> str :
140
- """Generates a 32-byte long random string that will be inserted into the
141
- user's renewal email's unique link, then saves it into the database.
158
+ async def generate_authenticated_renewal_token (self , user_id : str ) -> str :
159
+ """Generates a 8-digit long random string then saves it into the database.
160
+
161
+ This token is to be sent to the user over email so that the user can copy it into
162
+ their client to renew their account.
163
+
164
+ Args:
165
+ user_id: ID of the user to generate a string for.
166
+
167
+ Returns:
168
+ The generated string.
169
+
170
+ Raises:
171
+ SynapseError(500): Couldn't generate a unique string after 5 attempts.
172
+ """
173
+ renewal_token = random_digit_string (8 )
174
+ await self ._store .set_renewal_token_for_user (
175
+ user_id , renewal_token , TokenFormat .SHORT ,
176
+ )
177
+ return renewal_token
178
+
179
+ async def generate_unauthenticated_renewal_token (self , user_id : str ) -> str :
180
+ """Generates a 32-letter long random string then saves it into the database.
181
+
182
+ This token is to be sent to the user over email in a link that the user will then
183
+ click to renew their account.
142
184
143
185
Args:
144
186
user_id: ID of the user to generate a string for.
@@ -153,13 +195,19 @@ async def generate_renewal_token(self, user_id: str) -> str:
153
195
while attempts < 5 :
154
196
try :
155
197
renewal_token = random_string (32 )
156
- await self ._store .set_renewal_token_for_user (user_id , renewal_token )
198
+ await self ._store .set_renewal_token_for_user (
199
+ user_id , renewal_token , TokenFormat .LONG ,
200
+ )
157
201
return renewal_token
158
202
except SynapseError :
159
203
attempts += 1
160
204
raise SynapseError (500 , "Couldn't generate a unique string as refresh string." )
161
205
162
- async def renew_account (self , renewal_token : str ) -> Tuple [bool , bool , int ]:
206
+ async def renew_account (
207
+ self ,
208
+ renewal_token : str ,
209
+ user_id : Optional [str ] = None ,
210
+ ) -> Tuple [bool , bool , int ]:
163
211
"""Renews the account attached to a given renewal token by pushing back the
164
212
expiration date by the current validity period in the server's configuration.
165
213
@@ -169,19 +217,42 @@ async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]:
169
217
170
218
Args:
171
219
renewal_token: Token sent with the renewal request.
220
+ user_id: The Matrix ID of the user to renew, if the renewal request was
221
+ authenticated.
222
+
172
223
Returns:
173
224
A tuple containing:
174
225
* A bool representing whether the token is valid and unused.
175
226
* A bool which is `True` if the token is valid, but stale.
176
227
* An int representing the user's expiry timestamp as milliseconds since the
177
228
epoch, or 0 if the token was invalid.
178
229
"""
230
+ # Try to match the token against a known format.
231
+ if LONG_TOKEN_REGEX .match (renewal_token ):
232
+ token_format = TokenFormat .LONG
233
+ elif SHORT_TOKEN_REGEX .match (renewal_token ):
234
+ token_format = TokenFormat .SHORT
235
+ else :
236
+ # If we can't figure out what format the renewal token is, consider it
237
+ # invalid.
238
+ return False , False , 0
239
+
240
+ # If we were not able to authenticate the user requesting a renewal, and the
241
+ # token needs authentication, consider the token neither valid nor stale.
242
+ if user_id is None and token_format == TokenFormat .SHORT :
243
+ return False , False , 0
244
+
245
+ # Verify if the token, or the (token, user_id) tuple, exists.
179
246
try :
180
247
(
181
248
user_id ,
182
249
current_expiration_ts ,
183
250
token_used_ts ,
184
- ) = await self ._store .get_user_from_renewal_token (renewal_token )
251
+ ) = await self ._store .validate_renewal_token (
252
+ renewal_token ,
253
+ token_format ,
254
+ user_id ,
255
+ )
185
256
except SynapseError :
186
257
return False , False , 0
187
258
@@ -238,6 +309,7 @@ async def renew_account_for_user(
238
309
user_id = user_id ,
239
310
expiration_ts = expiration_ts ,
240
311
email_sent = email_sent ,
312
+ token_format = TokenFormat .LONG if self ._send_links else TokenFormat .SHORT ,
241
313
renewal_token = renewal_token ,
242
314
token_used_ts = now ,
243
315
)
0 commit comments