Newer
Older
postorius / src / postorius / models.py
@Simon Hanna Simon Hanna on 15 Mar 2016 9 KB Fix indentation and line length
# -*- 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 uuid
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.mail import send_mail
from django.db.models.signals import post_save
from django.core.urlresolvers import reverse
from django.dispatch import receiver
from django.db import models
from django.http import Http404
from django.template import Context
from django.template.loader import render_to_string
from mailmanclient import MailmanConnectionError
from postorius.utils import get_client
try:
    from urllib2 import HTTPError
except ImportError:
    from urllib.error import HTTPError

logger = logging.getLogger(__name__)


@receiver(post_save, sender=User)
def create_mailman_user(sender, **kwargs):
    if kwargs.get('created'):
        autocreate = False
        try:
            autocreate = settings.AUTOCREATE_MAILMAN_USER
        except AttributeError:
            pass
        if autocreate:
            user = kwargs.get('instance')
            try:
                MailmanUser.objects.create_from_django(user)
            except (MailmanApiError, HTTPError):
                pass


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 as e:
            raise MailmanApiError(e)

    def get(self, *args, **kwargs):
        try:
            method = getattr(get_client(), 'get_' + self.resource_name)
            return method(*args, **kwargs)
        except AttributeError as e:
            raise MailmanApiError(e)
        except HTTPError as e:
            if e.code == 404:
                raise Mailman404Error('Mailman resource could not be found.')
            else:
                raise
        except MailmanConnectionError as e:
            raise MailmanApiError(e)

    def get_or_404(self, *args, **kwargs):
        """Similar to `self.get` but raises standard Django 404 error.
        """
        try:
            return self.get(*args, **kwargs)
        except Mailman404Error:
            raise Http404
        except MailmanConnectionError as e:
            raise MailmanApiError(e)

    def create(self, *args, **kwargs):
        try:
            method = getattr(get_client(), 'create_' + self.resource_name)
            return method(*args, **kwargs)
        except AttributeError as e:
            raise MailmanApiError(e)
        except HTTPError as 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 as 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 MailmanUserManager(MailmanRestManager):

    def __init__(self):
        super(MailmanUserManager, self).__init__('user', 'users')

    def create_from_django(self, user):
        return self.create(user.email, user.get_full_name())

    def get_or_create_from_django(self, user):
        try:
            return self.get(address=user.email)
        except Mailman404Error:
            return self.create_from_django(user)


class MailmanRestModel(object):
    """Simple REST Model class to make REST API calls Django style.
    """
    MailmanApiError = MailmanApiError
    DoesNotExist = Mailman404Error

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def save(self):
        """Proxy function for `objects.create`.
        (REST API uses `create`, while Django uses `save`.)
        """
        self.objects.create(*self.args, **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 = MailmanUserManager()


class Member(MailmanRestModel):
    """Member model class.
    """
    objects = MailmanRestManager('member', 'members')


class AddressConfirmationProfile(models.Model):
    """
    Profile model for temporarily storing an activation key to register
    an email address.
    """
    email = models.EmailField(unique=True)
    activation_key = models.CharField(max_length=32, unique=True)
    created = models.DateTimeField(auto_now=True)
    user = models.ForeignKey(User)

    def save(self, *args, **kwargs):
        self.activation_key = uuid.uuid4().hex
        super(AddressConfirmationProfile, self).save(*args, **kwargs)

    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 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/user/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
        """
        # Get the url string from url conf.
        url = reverse('address_activation_link',
                      kwargs={'activation_key': self.activation_key})
        activation_link = request.build_absolute_uri(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/user/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 = {'activation_link': activation_link,
                                'host_url': request.build_absolute_uri("/")}
        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,
                  render_to_string(template_path, template_context),
                  sender_address,
                  [self.email])