Commit 771e86e9 authored by Aurélien Bompard's avatar Aurélien Bompard

Merge branch 'allauth'

Use django-allauth for authentication.
parents a221c901 6df09ffe
......@@ -4,7 +4,6 @@ omit =
src/postorius/tests/*.py
src/postorius/tests/*/*.py
src/postorius/doc/*.py
src/postorius/south_migrations/*.py
src/postorius/migrations/*.py
[html]
......
......@@ -2,15 +2,15 @@ image: maxking/mailman-ci-runner
django-1.8:
script:
- tox -e py27-django18
- tox -e py27-django18-head-coverage
django-1.9:
script:
- tox -e py27-django19
- tox -e py27-django19-head-coverage
django-1.10:
script:
- tox -e py27-django110
- tox -e py27-django110-head-coverage
pep8:
script:
......@@ -18,5 +18,5 @@ pep8:
django-latest:
script:
- tox -e py27-django-latest
- tox -e py27-django-latest-head
allow_failure: true
......@@ -35,8 +35,7 @@ Requirements
============
Postorius requires Python 2.7 or newer and mailmanclient,
the official Python bindings for GNU Mailman, it also requires
django-browserid.
the official Python bindings for GNU Mailman.
The minimum Django version is 1.8.
Postorius needs a running version of GNU Mailman version 3.
......
postorius.db
static
venv
settings_local.py
logs/*.log
......@@ -39,12 +39,14 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET_KEY = '$!-7^wl#wiifjbh)5@f7ji%x!vp7s1vzbvwt26hxv$idixq0u0'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = False
ADMINS = (
#('Admin', 'webmaster@example.com'),
)
SITE_ID = 1
ALLOWED_HOSTS = []
# Mailman API credentials
......@@ -60,12 +62,26 @@ INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'postorius',
'django_browserid',
'django_mailman3',
'django_gravatar',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.openid',
'django_mailman3.lib.auth.fedora',
'allauth.socialaccount.providers.github',
'allauth.socialaccount.providers.gitlab',
'allauth.socialaccount.providers.google',
#'allauth.socialaccount.providers.facebook',
'allauth.socialaccount.providers.twitter',
'allauth.socialaccount.providers.stackexchange',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
......@@ -83,6 +99,7 @@ MIDDLEWARE_CLASSES = (
# is the only app you want to serve.
ROOT_URLCONF = 'urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
......@@ -99,6 +116,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django_mailman3.context_processors.common',
'postorius.context_processors.postorius',
],
},
......@@ -164,18 +182,16 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
LOGIN_URL = 'user_login'
LOGIN_URL = 'account_login'
LOGIN_REDIRECT_URL = 'list_index'
LOGOUT_URL = 'user_logout'
LOGOUT_URL = 'account_logout'
# Use the email username as identifier, but truncate it because
# the User.username field is only 30 chars long.
def username(email):
return email.rsplit('@', 1)[0][:30]
BROWSERID_USERNAME_ALGO = username
# From Address for emails sent to users
DEFAULT_FROM_EMAIL = 'postorius@localhost.local'
# From Address for emails sent to admins
SERVER_EMAIL = 'root@localhost.local'
# Compatibility with Bootstrap 3
from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
......@@ -184,37 +200,93 @@ MESSAGE_TAGS = {
AUTHENTICATION_BACKENDS = (
'django_browserid.auth.BrowserIDBackend',
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
# Django Allauth
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
ACCOUNT_UNIQUE_EMAIL = True
SOCIALACCOUNT_PROVIDERS = {
'openid': {
'SERVERS': [
dict(id='yahoo',
name='Yahoo',
openid_url='http://me.yahoo.com'),
],
},
'google': {
'SCOPE': ['profile', 'email'],
'AUTH_PARAMS': {'access_type': 'online'},
},
'facebook': {
'METHOD': 'oauth2',
'SCOPE': ['email'],
'FIELDS': [
'email',
'name',
'first_name',
'last_name',
'locale',
'timezone',
],
'VERSION': 'v2.4',
},
}
# From Address for emails sent to users
DEFAULT_FROM_EMAIL = 'postorius@localhost.local'
# From Address for emails sent to admins
SERVER_EMAIL = 'root@localhost.local'
# These can be set to override the defaults but are not mandatory:
# EMAIL_CONFIRMATION_TEMPLATE = 'postorius/address_confirmation_message.txt'
# EMAIL_CONFIRMATION_SUBJECT = 'Confirmation needed'
# You can enable logging by uncommenting the following lines
# LOGGING = {
# 'version': 1,
# 'disable_existing_loggers': False,
# 'handlers': {
# 'console': {
# 'class': 'logging.StreamHandler'
# },
# },
# 'loggers': {
# 'django': {
# 'handlers': ['console'],
# 'level': 'INFO',
# },
# 'django_browserid': {
# 'handlers': ['console'],
# 'level': 'DEBUG',
# },
# },
# }
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
'file':{
'level': 'INFO',
#'class': 'logging.handlers.RotatingFileHandler',
'class': 'logging.handlers.WatchedFileHandler',
'filename': os.path.join(BASE_DIR, 'logs', 'postorius.log'),
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
'django.request': {
'handlers': ['console', 'file'],
'level': 'ERROR',
},
'postorius': {
'handlers': ['console', 'file'],
'level': 'INFO',
},
},
'formatters': {
'simple': {
'format': '%(levelname)s: %(message)s'
},
'verbose': {
'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s'
},
},
}
try:
from settings_local import *
except ImportError:
pass
......@@ -26,3 +26,34 @@ from settings import *
MAILMAN_REST_API_URL = 'http://localhost:9001'
MAILMAN_REST_API_USER = 'restadmin'
MAILMAN_REST_API_PASS = 'restpass'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
},
'django.request': {
'handlers': ['console'],
'level': 'ERROR',
},
'postorius': {
'handlers': ['console'],
'level': 'INFO',
},
},
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s'
},
},
}
......@@ -18,15 +18,18 @@
from django.conf.urls import include, url
from django.contrib import admin
admin.autodiscover()
from postorius.views import list as list_views
from django.core.urlresolvers import reverse_lazy
from django.views.generic import RedirectView
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url('', include('django_browserid.urls')),
url(r'^$', list_views.list_index),
url(r'^$', RedirectView.as_view(
url=reverse_lazy('postorius.views.list.list_index'),
permanent=True)),
url(r'^postorius/', include('postorius.urls')),
#url(r'^hyperkitty/', include('hyperkitty.urls')),
url(r'', include('django_mailman3.urls')),
url(r'^accounts/', include('allauth.urls')),
# Django admin
url(r'^admin/', include(admin.site.urls)),
]
......@@ -36,7 +36,7 @@ setup(
install_requires=[
'Django>=1.8',
'Django<1.10',
'django-browserid',
'django-mailman3',
'mailmanclient',
],
)
......@@ -16,4 +16,7 @@
# 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, unicode_literals
__version__ = '1.0.2'
default_app_config = 'postorius.apps.PostoriusConfig'
# -*- coding: utf-8 -*-
# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
# Copyright (C) 2016 by the Free Software Foundation, Inc.
#
# This file is part of Postorius.
#
......@@ -16,21 +16,12 @@
# You should have received a copy of the GNU General Public License along with
# Postorius. If not, see <http://www.gnu.org/licenses/>.
from django import template
from django.utils.html import conditional_escape
from __future__ import absolute_import, unicode_literals
register = template.Library()
from django.apps import AppConfig
@register.simple_tag(takes_context=True)
def add_to_query_string(context, *args, **kwargs):
"""Adds or replaces parameters in the query string"""
qs = context["request"].GET.copy()
# create a dict from every args couple
new_qs_elements = dict(zip(args[::2], args[1::2]))
new_qs_elements.update(kwargs)
# don't use the .update() method, it appends instead of overwriting.
for key, value in new_qs_elements.iteritems():
qs[key] = value
return conditional_escape(qs.urlencode())
class PostoriusConfig(AppConfig):
name = 'postorius'
verbose_name = "Postorius"
......@@ -15,8 +15,10 @@
#
# You should have received a copy of the GNU General Public License along with
# Postorius. If not, see <http://www.gnu.org/licenses/>.
"""Postorius view decorators."""
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import authenticate, login
from django.core.exceptions import PermissionDenied
......
......@@ -20,17 +20,18 @@
Authentication and authorization-related utilities.
"""
from __future__ import absolute_import, unicode_literals
from allauth.account.models import EmailAddress
from django.utils import six
from postorius.utils import set_other_emails
from postorius.models import List
def user_is_in_list_roster(user, mailing_list, roster):
if not user.is_authenticated():
return False
if not hasattr(user, 'other_emails'):
set_other_emails(user)
addresses = set([user.email]) | set(user.other_emails)
addresses = set(EmailAddress.objects.filter(
user=user, verified=True).values_list("email", flat=True))
if addresses & set(getattr(mailing_list, roster)):
return True # At least one address is in the roster
return False
......
......@@ -16,12 +16,9 @@
# You should have received a copy of the GNU General Public License along with
# Postorius. If not, see <http://www.gnu.org/licenses/>.
import logging
from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch
from django.shortcuts import resolve_url
from __future__ import absolute_import, unicode_literals
import logging
logger = logging.getLogger(__name__)
......@@ -36,19 +33,6 @@ def postorius(request):
else:
template_to_extend = "postorius/base.html"
# Find the HyperKitty URL if installed
hyperkitty_url = False
if "hyperkitty" in settings.INSTALLED_APPS:
try:
hyperkitty_url = reverse("hk_root")
except NoReverseMatch:
pass
return {
'postorius_base_template': template_to_extend,
'request': request,
'hyperkitty_url': hyperkitty_url,
# Resolve the login and logout URLs from the settings
'login_url': resolve_url(settings.LOGIN_URL),
'logout_url': resolve_url(settings.LOGOUT_URL),
}
......@@ -16,6 +16,8 @@
# 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, unicode_literals
from django.forms import Form
from django.utils import safestring
from django.forms.forms import BoundField
......
......@@ -16,12 +16,13 @@
# 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, unicode_literals
from django import forms
from django import __version__
from django.core.validators import validate_email
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from postorius.fieldset_forms import FieldsetForm
......@@ -68,13 +69,6 @@ class DomainNew(FieldsetForm):
required=True,
help_text=_('Example: domain.org'),
)
web_host = forms.URLField(
label=_('Web Host'),
error_messages={'required': _('Please enter a host name'),
'invalid': _('Please enter a valid host name.')},
required=True,
help_text=_('Example: http://www.domain.org'),
)
description = forms.CharField(
label=_('Description'),
required=False)
......@@ -98,7 +92,6 @@ class DomainNew(FieldsetForm):
"""
layout = [["Please enter Details",
"mail_host",
"web_host",
"description"]]
......@@ -828,35 +821,6 @@ class MemberModeration(FieldsetForm):
'the message. '))
class AddressActivationForm(forms.Form):
email = forms.EmailField(widget=forms.TextInput(
attrs={'placeholder': 'Enter alternate email'}))
def clean_email(self):
email = self.cleaned_data.get('email')
# Check if the address belongs to someone else
if User.objects.filter(email=email).exists():
raise forms.ValidationError(
_('This email is in use. Please choose another or contact'
' the administrator'), 'error')
return email
class ChangeDisplayNameForm(forms.Form):
"""
Change display name.
"""
display_name = forms.CharField(
label=_('Display name'),
error_messages={
'required': _('Please enter a display name')},
required=True
)
class ChangeSubscriptionForm(forms.Form):
email = forms.ChoiceField()
......
# Copyright (C) 2011-2012 by the Free Software Foundation, Inc.
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
"""Cleanse a message for archiving."""
from __future__ import absolute_import, unicode_literals
import os
import re
import binascii
from mimetypes import guess_all_extensions
from email.header import decode_header, make_header
from email.errors import HeaderParseError
pre = re.compile(r'[/\\:]')
sre = re.compile(r'[^-\w.]')
dre = re.compile(r'^\.*')
BR = '<br>\n'
NEXT_PART = re.compile(r'--------------[ ]next[ ]part[ ]--------------\n')
def guess_extension(ctype, ext):
all_exts = guess_all_extensions(ctype, strict=False)
if ext in all_exts:
return ext
return all_exts and all_exts[0]
def get_charset(message, default="ascii", guess=False):
if message.get_content_charset():
return message.get_content_charset().decode("ascii")
if message.get_charset():
return message.get_charset().decode("ascii")
charset = default
if not guess:
return charset
text = message.get_payload(decode=True)
for encoding in ["ascii", "utf-8", "iso-8859-15"]:
try:
text.decode(encoding)
except UnicodeDecodeError:
continue
else:
charset = encoding
break
return charset
def oneline(s):
"""Inspired by mailman.utilities.string.oneline"""
try:
h = make_header(decode_header(s))
ustr = h.__unicode__()
return ''.join(ustr.splitlines())
except (LookupError, UnicodeError, ValueError, HeaderParseError):
return ''.join(s.splitlines())
class Scrubber(object):
def __init__(self, msg):
self.msg = msg
def scrub(self):
attachments = []
for part_num, part in enumerate(self.msg.walk()):
ctype = part.get_content_type()
if not isinstance(ctype, unicode):
ctype = ctype.decode("ascii")
if ctype == 'text/plain':
disposition = part.get('content-disposition')
if disposition and disposition.decode(
"ascii", "replace").strip().startswith("attachment"):
attachments.append(self.parse_attachment(part, part_num))
part.set_payload('')
elif ctype == 'text/html':
attachments.append(self.parse_attachment(part, part_num,
filter_html=False))
part.set_payload('')
elif ctype == 'message/rfc822':
attachments.append(self.parse_attachment(part, part_num))
part.set_payload('')
elif part.get_payload() and not part.is_multipart():
payload = part.get_payload(decode=True)
ctype = part.get_content_type()
if not isinstance(ctype, unicode):
ctype.decode("ascii")
if payload is None:
continue
attachments.append(self.parse_attachment(part, part_num))
if self.msg.is_multipart():
text = []
for part in self.msg.walk():
if not part.get_payload() or part.is_multipart():