]>
Commit | Line | Data |
---|---|---|
9137135a MT |
1 | #!/usr/bin/python |
2 | ||
98b826d4 | 3 | import datetime |
b9c2a52b | 4 | import email.utils |
9137135a MT |
5 | import hashlib |
6 | import logging | |
f6e6ff79 | 7 | import pytz |
9137135a | 8 | import random |
f6e6ff79 | 9 | import re |
9137135a MT |
10 | import string |
11 | import urllib | |
8d8d65b4 | 12 | import ldap |
9137135a MT |
13 | |
14 | import tornado.locale | |
15 | ||
b9c2a52b MT |
16 | log = logging.getLogger("users") |
17 | log.propagate = 1 | |
18 | ||
2c909128 | 19 | from . import base |
b7debe9f | 20 | from . import ldap |
9137135a | 21 | |
b9c2a52b MT |
22 | from .decorators import * |
23 | ||
f6e6ff79 MT |
24 | # A list of possible random characters. |
25 | random_chars = string.ascii_letters + string.digits | |
26 | ||
27 | def generate_random_string(length=16): | |
28 | """ | |
29 | Return a string with random chararcters A-Za-z0-9 with given length. | |
30 | """ | |
31 | return "".join([random.choice(random_chars) for i in range(length)]) | |
32 | ||
33 | ||
34 | def generate_password_hash(password, salt=None, algo="sha512"): | |
35 | """ | |
36 | This function creates a salted digest of the given password. | |
37 | """ | |
38 | # Generate the salt (length = 16) of none was given. | |
39 | if salt is None: | |
40 | salt = generate_random_string(length=16) | |
41 | ||
42 | # Compute the hash. | |
43 | # <SALT> + <PASSWORD> | |
44 | if not algo in hashlib.algorithms: | |
45 | raise Exception, "Unsupported password hash algorithm: %s" % algo | |
46 | ||
47 | # Calculate the digest. | |
48 | h = hashlib.new(algo) | |
49 | h.update(salt) | |
50 | h.update(password) | |
51 | ||
52 | # Output string is of kind "<algo>$<salt>$<hash>". | |
53 | return "$".join((algo, salt, h.hexdigest())) | |
54 | ||
55 | def check_password_hash(password, password_hash): | |
56 | """ | |
57 | Check a plain-text password with the given digest. | |
58 | """ | |
59 | # Handle plaintext passwords (plain$<password>). | |
60 | if password_hash.startswith("plain$"): | |
61 | return password_hash[6:] == password | |
62 | ||
63 | try: | |
64 | algo, salt, digest = password_hash.split("$", 2) | |
65 | except ValueError: | |
66 | logging.warning("Unknown password hash: %s" % password_hash) | |
67 | return False | |
68 | ||
69 | # Re-generate the password hash and compare the result. | |
70 | return password_hash == generate_password_hash(password, salt=salt, algo=algo) | |
71 | ||
f6e6ff79 | 72 | |
9137135a | 73 | class Users(base.Object): |
8d8d65b4 | 74 | def init(self): |
b7debe9f | 75 | self.ldap = ldap.LDAP(self.backend) |
8d8d65b4 | 76 | |
b9c2a52b MT |
77 | def _get_user(self, query, *args): |
78 | res = self.db.get(query, *args) | |
9137135a | 79 | |
b9c2a52b MT |
80 | if res: |
81 | return User(self.backend, res.id, data=res) | |
9137135a | 82 | |
b9c2a52b MT |
83 | def _get_users(self, query, *args): |
84 | res = self.db.query(query, *args) | |
8d8d65b4 | 85 | |
b9c2a52b MT |
86 | for row in res: |
87 | yield User(self.backend, row.id, data=row) | |
9137135a | 88 | |
b9c2a52b MT |
89 | def _get_user_email(self, query, *args): |
90 | res = self.db.get(query, *args) | |
f6e6ff79 | 91 | |
b9c2a52b MT |
92 | if res: |
93 | return UserEmail(self.backend, res.id, data=res) | |
f6e6ff79 | 94 | |
b9c2a52b MT |
95 | def _get_user_emails(self, query, *args): |
96 | res = self.db.query(query, *args) | |
f6e6ff79 | 97 | |
b9c2a52b MT |
98 | for row in res: |
99 | yield UserEmail(self.backend, row.id, data=row) | |
9137135a | 100 | |
b9c2a52b MT |
101 | def __iter__(self): |
102 | users = self._get_users("SELECT * FROM users \ | |
103 | WHERE activated IS TRUE AND deleted IS FALSE ORDER BY name") | |
8d8d65b4 | 104 | |
b9c2a52b | 105 | return iter(users) |
8d8d65b4 | 106 | |
b9c2a52b MT |
107 | def __len__(self): |
108 | res = self.db.get("SELECT COUNT(*) AS count FROM users \ | |
109 | WHERE activated IS TRUE AND deleted IS FALSE") | |
8d8d65b4 | 110 | |
b9c2a52b | 111 | return res.count |
9137135a | 112 | |
b9c2a52b MT |
113 | def create(self, name, realname=None, ldap_dn=None): |
114 | # XXX check if username has the correct name | |
9137135a | 115 | |
b9c2a52b MT |
116 | # Check if name is already taken |
117 | user = self.get_by_name(name) | |
118 | if user: | |
119 | raise ValueError("Username %s already taken" % name) | |
9137135a | 120 | |
b9c2a52b MT |
121 | # Create new user |
122 | user = self._get_user("INSERT INTO users(name, realname, ldap_dn) \ | |
123 | VALUES(%s, %s, %s) RETURNING *", name, realname, ldap_dn) | |
9137135a | 124 | |
b9c2a52b MT |
125 | # Create row in permissions table. |
126 | self.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", user.id) | |
9137135a | 127 | |
b9c2a52b | 128 | log.debug("Created user %s" % user.name) |
9137135a | 129 | |
b9c2a52b | 130 | return user |
9137135a | 131 | |
b9c2a52b MT |
132 | def create_from_ldap(self, name): |
133 | log.debug("Creating user %s from LDAP" % name) | |
9137135a | 134 | |
b9c2a52b MT |
135 | # Get required attributes from LDAP |
136 | dn, attr = self.ldap.get_user(name, attrlist=["uid", "cn", "mail"]) | |
137 | assert dn | |
9137135a | 138 | |
b9c2a52b MT |
139 | # Create regular user |
140 | user = self.create(name, realname=attr["cn"][0], ldap_dn=dn) | |
141 | user.activate() | |
9137135a | 142 | |
b9c2a52b MT |
143 | # Add all email addresses and activate them |
144 | for email in attr["mail"]: | |
145 | user.add_email(email, activated=True) | |
9137135a | 146 | |
b9c2a52b | 147 | return user |
9137135a | 148 | |
b9c2a52b MT |
149 | def auth(self, name, password): |
150 | # If either name or password is None, we don't check at all. | |
151 | if None in (name, password): | |
152 | return | |
9137135a | 153 | |
ba1958a5 JS |
154 | # usually we will get an email address as name |
155 | user = self.get_by_email(name) or self.get_by_name(name) | |
f6e6ff79 | 156 | |
b9c2a52b | 157 | if not user: |
ba1958a5 JS |
158 | # If no user could be found, we search for a matching user in |
159 | # the LDAP database | |
b9c2a52b MT |
160 | if not self.ldap.auth(name, password): |
161 | return | |
f6e6ff79 | 162 | |
b9c2a52b MT |
163 | # If a LDAP user is found (and password matches), we will |
164 | # create a new local user with the information from LDAP. | |
ba1958a5 JS |
165 | user = self.create_from_ldap(name) |
166 | ||
167 | if not user.activated or user.deleted: | |
168 | return | |
b9c2a52b MT |
169 | |
170 | # Check if the password matches | |
171 | if user.check_password(password): | |
172 | return user | |
c9619eec | 173 | |
b9c2a52b MT |
174 | def email_in_use(self, email): |
175 | return self._get_user_email("SELECT * FROM users_emails \ | |
176 | WHERE email = %s AND activated IS TRUE", email) | |
f6e6ff79 | 177 | |
b9c2a52b MT |
178 | def get_by_id(self, id): |
179 | return self._get_user("SELECT * FROM users WHERE id = %s", id) | |
f6e6ff79 | 180 | |
b9c2a52b MT |
181 | def get_by_name(self, name): |
182 | return self._get_user("SELECT * FROM users WHERE name = %s", name) | |
f6e6ff79 | 183 | |
b9c2a52b MT |
184 | def get_by_email(self, email): |
185 | return self._get_user("SELECT users.* FROM users \ | |
186 | LEFT JOIN users_emails ON users.id = users_emails.user_id \ | |
187 | WHERE users_emails.email = %s", email) | |
f6e6ff79 | 188 | |
98b826d4 JS |
189 | def get_by_password_recovery_code(self, code): |
190 | return self._get_user("SELECT * FROM users \ | |
191 | WHERE password_recovery_code = %s AND password_recovery_code_expires_at > NOW()", code) | |
192 | ||
040fc249 MT |
193 | def find_maintainers(self, maintainers): |
194 | email_addresses = [] | |
195 | ||
196 | # Make a unique list of all email addresses | |
197 | for maintainer in maintainers: | |
198 | name, email_address = email.utils.parseaddr(maintainer) | |
199 | ||
200 | if not email_address in email_addresses: | |
201 | email_addresses.append(email_address) | |
202 | ||
203 | users = self._get_users("SELECT DISTINCT users.* FROM users \ | |
204 | LEFT JOIN users_emails ON users.id = users_emails.user_id \ | |
205 | WHERE users_emails.activated IS TRUE \ | |
206 | AND users_emails.email = ANY(%s)", email_addresses) | |
207 | ||
208 | return sorted(users) | |
209 | ||
f6e6ff79 | 210 | def find_maintainer(self, s): |
b9c2a52b | 211 | name, email_address = email.utils.parseaddr(s) |
f6e6ff79 | 212 | |
b9c2a52b MT |
213 | # Got invalid input |
214 | if not email_address: | |
f6e6ff79 MT |
215 | return |
216 | ||
b9c2a52b MT |
217 | return self.get_by_email(email_address) |
218 | ||
219 | def search(self, pattern, limit=None): | |
220 | pattern = "%%%s%%" % pattern | |
221 | ||
efbd7501 | 222 | users = self._get_users("SELECT * FROM users \ |
b9c2a52b MT |
223 | WHERE (name LIKE %s OR realname LIKE %s) \ |
224 | AND activated IS TRUE AND deleted IS FALSE \ | |
225 | ORDER BY name LIMIT %s", pattern, pattern, limit) | |
f6e6ff79 | 226 | |
efbd7501 MT |
227 | return list(users) |
228 | ||
c0b302b3 JS |
229 | @staticmethod |
230 | def check_password_strength(password): | |
231 | score = 0 | |
232 | accepted = False | |
233 | ||
234 | # Empty passwords cannot be used. | |
235 | if len(password) == 0: | |
236 | return False, 0 | |
237 | ||
238 | # Passwords with less than 6 characters are also too weak. | |
239 | if len(password) < 6: | |
240 | return False, 1 | |
241 | ||
242 | # Password with at least 8 characters are secure. | |
243 | if len(password) >= 8: | |
244 | score += 1 | |
245 | ||
246 | # 10 characters are even more secure. | |
247 | if len(password) >= 10: | |
248 | score += 1 | |
249 | ||
250 | # Digits in the password are good. | |
251 | if re.search("\d+", password): | |
252 | score += 1 | |
253 | ||
254 | # Check for lowercase AND uppercase characters. | |
255 | if re.search("[a-z]", password) and re.search("[A-Z]", password): | |
256 | score += 1 | |
257 | ||
258 | # Search for special characters. | |
259 | if re.search(".[!,@,#,$,%,^,&,*,?,_,~,-,(,)]", password): | |
260 | score += 1 | |
261 | ||
262 | if score >= 3: | |
263 | accepted = True | |
264 | ||
265 | return accepted, score | |
f6e6ff79 | 266 | |
9137135a | 267 | |
b9c2a52b MT |
268 | class User(base.DataObject): |
269 | table = "users" | |
9137135a | 270 | |
20d7f5eb MT |
271 | def __repr__(self): |
272 | return "<%s %s>" % (self.__class__.__name__, self.realname) | |
273 | ||
b0315eb4 MT |
274 | def __hash__(self): |
275 | return hash(self.id) | |
276 | ||
b9c2a52b MT |
277 | def __eq__(self, other): |
278 | if isinstance(other, self.__class__): | |
279 | return self.id == other.id | |
f6e6ff79 | 280 | |
b9c2a52b MT |
281 | def __lt__(self, other): |
282 | if isinstance(other, self.__class__): | |
283 | return self.name < other.name | |
f6e6ff79 | 284 | |
b9c2a52b MT |
285 | elif isinstance(other, str): |
286 | return self.name < other | |
f6e6ff79 | 287 | |
9137135a | 288 | def delete(self): |
b9c2a52b | 289 | self._set_attribute("deleted", True) |
9137135a MT |
290 | |
291 | def activate(self): | |
b9c2a52b | 292 | self._set_attribute("activated", True) |
f6e6ff79 MT |
293 | |
294 | def check_password(self, password): | |
295 | """ | |
296 | Compare the given password with the one stored in the database. | |
297 | """ | |
8d8d65b4 JS |
298 | if self.ldap_dn: |
299 | return self.backend.users.ldap.bind(self.ldap_dn, password) | |
300 | ||
f6e6ff79 | 301 | return check_password_hash(password, self.data.passphrase) |
9137135a MT |
302 | |
303 | def set_passphrase(self, passphrase): | |
304 | """ | |
305 | Update the passphrase the users uses to log on. | |
306 | """ | |
98b826d4 JS |
307 | # We cannot set the password for ldap users |
308 | if self.ldap_dn: | |
309 | raise AttributeError("Cannot set passphrase for LDAP user") | |
310 | ||
f6e6ff79 MT |
311 | self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s", |
312 | generate_password_hash(passphrase), self.id) | |
9137135a MT |
313 | |
314 | passphrase = property(lambda x: None, set_passphrase) | |
315 | ||
9137135a | 316 | def get_realname(self): |
b9c2a52b | 317 | return self.data.realname or self.name |
9137135a MT |
318 | |
319 | def set_realname(self, realname): | |
b9c2a52b | 320 | self._set_attribute("realname", realname) |
9137135a MT |
321 | |
322 | realname = property(get_realname, set_realname) | |
323 | ||
324 | @property | |
325 | def name(self): | |
326 | return self.data.name | |
327 | ||
8d8d65b4 JS |
328 | @property |
329 | def ldap_dn(self): | |
330 | return self.data.ldap_dn | |
331 | ||
f6e6ff79 MT |
332 | @property |
333 | def firstname(self): | |
3f82e940 MT |
334 | # Try to split the string into first and last name. |
335 | # If that is not successful, return the entire realname. | |
336 | try: | |
337 | firstname, rest = self.realname.split(" ", 1) | |
338 | except: | |
339 | return self.realname | |
f6e6ff79 MT |
340 | |
341 | return firstname | |
342 | ||
d0bce25d MT |
343 | @property |
344 | def envelope_from(self): | |
345 | return "%s <%s>" % (self.realname, self.email) | |
346 | ||
b9c2a52b MT |
347 | @lazy_property |
348 | def emails(self): | |
349 | res = self.backend.users._get_user_emails("SELECT * FROM users_emails \ | |
350 | WHERE user_id = %s AND activated IS TRUE ORDER BY email", self.id) | |
351 | ||
352 | return list(res) | |
353 | ||
26fe80df JS |
354 | @property |
355 | def email(self): | |
b9c2a52b MT |
356 | for email in self.emails: |
357 | if email.primary: | |
358 | return email | |
f6e6ff79 | 359 | |
b9c2a52b MT |
360 | def get_email(self, email): |
361 | for e in self.emails: | |
362 | if e == email: | |
363 | return e | |
9137135a | 364 | |
26fe80df JS |
365 | def set_primary_email(self, email): |
366 | if not email in self.emails: | |
367 | raise ValueError("Email address does not belong to user") | |
9137135a | 368 | |
26fe80df JS |
369 | # Mark previous primary email as non-primary |
370 | self.db.execute("UPDATE users_emails SET \"primary\" = FALSE \ | |
371 | WHERE user_id = %s AND \"primary\" IS TRUE" % self.id) | |
f6e6ff79 | 372 | |
26fe80df JS |
373 | # Mark new primary email |
374 | self.db.execute("UPDATE users_emails SET \"primary\" = TRUE \ | |
375 | WHERE user_id = %s AND email = %s AND activated IS TRUE", | |
376 | self.id, email) | |
9137135a | 377 | |
040fc249 MT |
378 | def has_email_address(self, email_address): |
379 | try: | |
380 | mail, email_address = email.utils.parseaddr(email_address) | |
381 | except: | |
382 | pass | |
383 | ||
384 | return email_address in self.emails | |
385 | ||
26fe80df | 386 | def activate_email(self, code): |
b9c2a52b MT |
387 | # Search email by activation code |
388 | email = self.backend.users._get_user_email("SELECT * FROM users_emails \ | |
389 | WHERE user_id = %s AND activated IS FALSE AND activation_code = %s", self.id, code) | |
26fe80df | 390 | |
b9c2a52b | 391 | if not email: |
26fe80df JS |
392 | return False |
393 | ||
b9c2a52b MT |
394 | # Activate email address |
395 | email.activate() | |
26fe80df JS |
396 | return True |
397 | ||
398 | # Te activated flag is useful for LDAP users | |
399 | def add_email(self, email, activated=False): | |
400 | # Check if the email is in use | |
b9c2a52b | 401 | if self.backend.users.email_in_use(email): |
26fe80df JS |
402 | raise ValueError("Email %s is already in use" % email) |
403 | ||
404 | activation_code = None | |
405 | if not activated: | |
406 | activation_code = generate_random_string(64) | |
407 | ||
b9c2a52b MT |
408 | user_email = self.backend.users._get_user_email("INSERT INTO users_emails(user_id, email, \ |
409 | \"primary\", activated, activation_code) VALUES(%s, %s, %s, %s, %s) RETURNING *", | |
410 | self.id, email, not self.emails, activated, activation_code) | |
7b5528b5 MT |
411 | |
412 | # Set caches | |
413 | user_email.user = self | |
b9c2a52b | 414 | self.emails.append(user_email) |
26fe80df | 415 | |
b9c2a52b | 416 | # Send activation email if activation is needed |
8d8d65b4 | 417 | if not activated: |
b9c2a52b | 418 | user_email.send_activation_mail() |
9137135a | 419 | |
b9c2a52b | 420 | return user_email |
9137135a | 421 | |
68dd077d MT |
422 | def send_template(self, *args, **kwargs): |
423 | return self.backend.messages.send_template(self, *args, **kwargs) | |
424 | ||
b9c2a52b | 425 | def is_admin(self): |
e304790f | 426 | return self.data.admin is True |
9137135a | 427 | |
4947da2d MT |
428 | def get_locale(self): |
429 | return tornado.locale.get(self.data.locale) | |
430 | ||
9137135a | 431 | def set_locale(self, locale): |
b9c2a52b | 432 | self._set_attribute("locale", locale) |
9137135a | 433 | |
4947da2d | 434 | locale = property(get_locale, set_locale) |
9137135a | 435 | |
f6e6ff79 MT |
436 | def get_timezone(self, tz=None): |
437 | if tz is None: | |
438 | tz = self.data.timezone or "" | |
439 | ||
440 | try: | |
441 | tz = pytz.timezone(tz) | |
442 | except pytz.UnknownTimeZoneError: | |
443 | tz = pytz.timezone("UTC") | |
444 | ||
445 | return tz | |
446 | ||
447 | def set_timezone(self, timezone): | |
448 | if not timezone is None: | |
449 | tz = self.get_timezone(timezone) | |
450 | timezone = tz.zone | |
451 | ||
b9c2a52b | 452 | self._set_attribute("timezone", timezone) |
f6e6ff79 MT |
453 | |
454 | timezone = property(get_timezone, set_timezone) | |
455 | ||
98b826d4 JS |
456 | def get_password_recovery_code(self): |
457 | return self.data.password_recovery_code | |
458 | ||
459 | def set_password_recovery_code(self, code): | |
460 | self._set_attribute("password_recovery_code", code) | |
461 | ||
462 | self._set_attribute("password_recovery_code_expires_at", | |
463 | datetime.datetime.utcnow() + datetime.timedelta(days=1)) | |
464 | ||
465 | password_recovery_code = property(get_password_recovery_code, set_password_recovery_code) | |
466 | ||
467 | def forgot_password(self): | |
468 | log.debug("User %s reqested password recovery" % self.name) | |
469 | ||
470 | # We cannot reset te password for ldap users | |
471 | if self.ldap_dn: | |
472 | # Maybe we should send an email with an explanation | |
473 | return | |
474 | ||
475 | # Add a recovery code to the database and a timestamp when this code expires | |
476 | self.password_recovery_code = generate_random_string(64) | |
477 | ||
a76becd9 JS |
478 | # Send an email with the activation code |
479 | self.send_template("messages/users/password-reset", user=self) | |
98b826d4 | 480 | |
9137135a MT |
481 | @property |
482 | def activated(self): | |
26fe80df | 483 | return self.data.activated |
9137135a | 484 | |
ba1958a5 JS |
485 | @property |
486 | def deleted(self): | |
487 | return self.data.deleted | |
488 | ||
9137135a | 489 | @property |
b01825aa MT |
490 | def registered_at(self): |
491 | return self.data.registered_at | |
9137135a MT |
492 | |
493 | def gravatar_icon(self, size=128): | |
9bf767c3 MT |
494 | h = hashlib.new("md5") |
495 | if self.email: | |
496 | h.update("%s" % self.email) | |
497 | ||
9137135a | 498 | # construct the url |
9bf767c3 | 499 | gravatar_url = "http://www.gravatar.com/avatar/%s?" % h.hexdigest() |
9137135a MT |
500 | gravatar_url += urllib.urlencode({'d': "mm", 's': str(size)}) |
501 | ||
502 | return gravatar_url | |
503 | ||
b9c2a52b | 504 | @lazy_property |
f6e6ff79 | 505 | def perms(self): |
b9c2a52b | 506 | return self.db.get("SELECT * FROM users_permissions WHERE user_id = %s", self.id) |
f6e6ff79 MT |
507 | |
508 | def has_perm(self, perm): | |
509 | """ | |
510 | Returns True if the user has the requested permission. | |
511 | """ | |
512 | # Admins have the permission for everything. | |
513 | if self.is_admin(): | |
514 | return True | |
515 | ||
f6e6ff79 | 516 | # All others must be checked individually. |
8d8d65b4 | 517 | return self.perms.get(perm, False) == True |
f6e6ff79 | 518 | |
b0315eb4 MT |
519 | @property |
520 | def sessions(self): | |
521 | return self.backend.sessions._get_sessions("SELECT * FROM sessions \ | |
522 | WHERE user_id = %s AND valid_until >= NOW() ORDER BY created_at") | |
523 | ||
b9c2a52b MT |
524 | |
525 | class UserEmail(base.DataObject): | |
526 | table = "users_emails" | |
527 | ||
528 | def __str__(self): | |
529 | return self.email | |
530 | ||
531 | def __eq__(self, other): | |
532 | if isinstance(other, self.__class__): | |
533 | return self.id == other.id | |
534 | ||
535 | elif isinstance(other, str): | |
536 | return self.email == other | |
537 | ||
538 | @lazy_property | |
539 | def user(self): | |
540 | return self.backend.users.get_by_id(self.data.user_id) | |
541 | ||
542 | @property | |
543 | def recipient(self): | |
e50f17fe | 544 | return "%s <%s>" % (self.user.realname, self.email) |
b9c2a52b MT |
545 | |
546 | @property | |
547 | def email(self): | |
548 | return self.data.email | |
549 | ||
550 | def set_primary(self, primary): | |
551 | self._set_attribute("primary", primary) | |
552 | ||
553 | primary = property(lambda s: s.data.primary, set_primary) | |
554 | ||
555 | @property | |
556 | def activated(self): | |
557 | return self.data.activated | |
558 | ||
559 | def activate(self): | |
560 | self._set_attribute("activated", True) | |
561 | self._set_attribute("activation_code", None) | |
562 | ||
563 | @property | |
564 | def activation_code(self): | |
565 | return self.data.activation_code | |
566 | ||
9137135a | 567 | def send_activation_mail(self): |
b9c2a52b MT |
568 | if not self.primary: |
569 | return self.send_email_activation_email() | |
570 | ||
9137135a MT |
571 | logging.debug("Sending activation mail to %s" % self.email) |
572 | ||
68dd077d | 573 | self.user.send_template("messages/users/account-activation") |
9137135a | 574 | |
9ad522a5 | 575 | def send_email_activation_mail(self): |
b9c2a52b | 576 | logging.debug("Sending email address activation mail to %s" % self.email) |
26fe80df | 577 | |
9ad522a5 | 578 | self.user.send_template("messages/users/email-activation", email=self) |
26fe80df | 579 | |
f6e6ff79 MT |
580 | |
581 | # Some testing code. | |
582 | if __name__ == "__main__": | |
583 | for password in ("1234567890", "abcdefghij"): | |
584 | digest = generate_password_hash(password) | |
585 | ||
586 | print "%s %s" % (password, digest) | |
587 | print " Matches? %s" % check_password_hash(password, digest) | |
9137135a | 588 |