Newer
Older
postorius / src / postorius / models.py
# -*- 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])