# -*- coding: utf-8 -*- # Copyright (C) 1998-2014 by the Free Software Foundation, Inc. # # This file is part of Postorius. # # Postorius is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # Postorius is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along with # Postorius. If not, see <http://www.gnu.org/licenses/>. from __future__ import ( absolute_import, division, print_function, unicode_literals) import random import hashlib import logging from datetime import datetime, timedelta from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.core.mail import send_mail from django.db import models from django.http import Http404 from django.template import Context from django.template.loader import get_template from mailmanclient import MailmanConnectionError from postorius.utils import get_client from urllib2 import HTTPError logger = logging.getLogger(__name__) class MailmanApiError(Exception): """Raised if the API is not available. """ pass class Mailman404Error(Exception): """Proxy exception. Raised if the API returns 404.""" pass class MailmanRestManager(object): """Manager class to give a model class CRUD access to the API. Returns objects (or lists of objects) retrived from the API. """ def __init__(self, resource_name, resource_name_plural, cls_name=None): self.resource_name = resource_name self.resource_name_plural = resource_name_plural def all(self): try: return getattr(get_client(), self.resource_name_plural) except AttributeError: raise MailmanApiError except MailmanConnectionError, e: raise MailmanApiError(e) def get(self, **kwargs): try: method = getattr(get_client(), 'get_' + self.resource_name) return method(**kwargs) except AttributeError, e: raise MailmanApiError(e) except HTTPError, e: if e.code == 404: raise Mailman404Error('Mailman resource could not be found.') else: raise except MailmanConnectionError, e: raise MailmanApiError(e) def get_or_404(self, **kwargs): """Similar to `self.get` but raises standard Django 404 error. """ try: return self.get(**kwargs) except Mailman404Error: raise Http404 except MailmanConnectionError, e: raise MailmanApiError(e) def create(self, **kwargs): try: method = getattr(get_client(), 'create_' + self.resource_name) return method(**kwargs) except AttributeError, e: raise MailmanApiError(e) except HTTPError, e: if e.code == 409: raise MailmanApiError else: raise except MailmanConnectionError: raise MailmanApiError def delete(self): """Not implemented since the objects returned from the API have a `delete` method of their own. """ pass class MailmanListManager(MailmanRestManager): def __init__(self): super(MailmanListManager, self).__init__('list', 'lists') def all(self, only_public=False): try: objects = getattr(get_client(), self.resource_name_plural) except AttributeError: raise MailmanApiError except MailmanConnectionError, e: raise MailmanApiError(e) if only_public: public = [] for obj in objects: if obj.settings.get('advertised', False): public.append(obj) return public else: return objects def by_mail_host(self, mail_host, only_public=False): objects = self.all(only_public) host_objects = [] for obj in objects: if obj.mail_host == mail_host: host_objects.append(obj) return host_objects class MailmanRestModel(object): """Simple REST Model class to make REST API calls Django style. """ MailmanApiError = MailmanApiError DoesNotExist = Mailman404Error def __init__(self, **kwargs): self.kwargs = kwargs def save(self): """Proxy function for `objects.create`. (REST API uses `create`, while Django uses `save`.) """ self.objects.create(**self.kwargs) class Domain(MailmanRestModel): """Domain model class. """ objects = MailmanRestManager('domain', 'domains') class List(MailmanRestModel): """List model class. """ objects = MailmanListManager() class MailmanUser(MailmanRestModel): """MailmanUser model class. """ objects = MailmanRestManager('user', 'users') class Member(MailmanRestModel): """Member model class. """ objects = MailmanRestManager('member', 'members') class AddressConfirmationProfileManager(models.Manager): """ Manager class for AddressConfirmationProfile. """ def create_profile(self, email, user): # Create or update a profile # Guarantee an email bytestr type that can be fed to hashlib. email_str = email if isinstance(email_str, unicode): email_str = email_str.encode('utf-8') activation_key = hashlib.sha1( str(random.random())+email_str).hexdigest() # Make now tz naive (we don't care about the timezone) now = datetime.now().replace(tzinfo=None) # Either update an existing profile record for the given email address try: profile = self.get(email=email) profile.activation_key = activation_key profile.created = now profile.save() # ... or create a new one. except AddressConfirmationProfile.DoesNotExist: profile = self.create(email=email, activation_key=activation_key, user=user, created=now) return profile class AddressConfirmationProfile(models.Model): """ Profile model for temporarily storing an activation key to register an email address. """ email = models.EmailField() activation_key = models.CharField(max_length=40) created = models.DateTimeField() user = models.ForeignKey(User) objects = AddressConfirmationProfileManager() def __unicode__(self): return u'Address Confirmation Profile for {0}'.format(self.email) @property def is_expired(self): """ a profile expires after 1 day by default. This can be configured in the settings. >>> EMAIL_CONFIRMATION_EXPIRATION_DELTA = timedelta(days=2) """ expiration_delta = getattr( settings, 'EMAIL_CONFIRMATION_EXPIRATION_DELTA', timedelta(days=1)) age = datetime.now().replace(tzinfo=None) - \ self.created.replace(tzinfo=None) return age > expiration_delta def _create_host_url(self, request): # Create the host url protocol = 'https' if not request.is_secure(): protocol = 'http' server_name = request.META['SERVER_NAME'] if server_name[-1] == '/': server_name = server_name[:len(server_name) - 1] return '{0}://{1}'.format(protocol, server_name) def send_confirmation_link(self, request, template_context=None, template_path=None): """ Send out a message containing a link to activate the given address. The following settings are recognized: >>> EMAIL_CONFIRMATION_TEMPLATE = 'postorius/address_confirmation_message.txt' >>> EMAIL_CONFIRMATION_FROM = 'Confirmation needed' >>> EMAIL_CONFIRMATION_SUBJECT = 'postmaster@list.org' :param request: The HTTP request object. :type request: HTTPRequest :param template_context: The context used when rendering the template. Falls back to host url and activation link. :type template_context: django.template.Context """ # create the host url and the activation link need for the template host_url = self._create_host_url(request) # Get the url string from url conf. url = reverse('address_activation_link', kwargs={'activation_key': self.activation_key}) activation_link = '{0}{1}'.format(host_url, url) # Detect the right template path, either from the param, # the setting or the default if not template_path: template_path = getattr(settings, 'EMAIL_CONFIRMATION_TEMPLATE', 'postorius/address_confirmation_message.txt') # Create a template context (if there is none) containing # the activation_link and the host_url. if not template_context: template_context = Context( {'activation_link': activation_link, 'host_url': host_url}) email_subject = getattr( settings, 'EMAIL_CONFIRMATION_SUBJECT', u'Confirmation needed') send_mail(email_subject, get_template(template_path).render(template_context), getattr(settings, 'EMAIL_CONFIRMATION_FROM'), [self.email])