Newer
Older
postorius / src / postorius / models.py
# -*- coding: utf-8 -*-
# Copyright (C) 1998-2015 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.exceptions import ImproperlyConfigured
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 = 'postmaster@list.org'
            >>> EMAIL_CONFIRMATION_SUBJECT = 'Confirmation needed'

        :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')
        try:
            sender_address = getattr(settings, 'EMAIL_CONFIRMATION_FROM')
        except AttributeError:
            # settings.EMAIL_CONFIRMATION_FROM is not defined, fallback
            # settings.DEFAULT_EMAIL_FROM as mentioned in the django
            # docs. If that also fails, raise a `ImproperlyConfigured` Error.
            try:
                sender_address = getattr(settings, 'DEFAULT_FROM_EMAIL')
            except AttributeError:
                raise ImproperlyConfigured

        send_mail(email_subject,
                  get_template(template_path).render(template_context),
                  sender_address,
                  [self.email])