574 lines
25 KiB
Python
574 lines
25 KiB
Python
import smtplib
|
|
from itertools import count
|
|
import socket
|
|
import urllib
|
|
import os.path
|
|
import logging
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
import django.contrib.auth as auth
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.http import HttpResponse, Http404, HttpResponseRedirect
|
|
from django.db.models.query import Q
|
|
from django.contrib.auth.models import User
|
|
from django.db import transaction
|
|
from django.contrib import messages
|
|
from django.utils.translation import ugettext as _, ungettext
|
|
from django.core.mail import EmailMultiAlternatives, EmailMessage
|
|
from django.template.loader import render_to_string
|
|
from django.core.validators import validate_email, ValidationError
|
|
from django.contrib.auth.tokens import default_token_generator
|
|
from django.utils.http import int_to_base36
|
|
from BeautifulSoup import BeautifulSoup
|
|
|
|
from forms import FileForm, AnonymousContactForm, ContactForm, DelegationForm, ForwardingForm
|
|
from models import Mailbox, username, Delegation, AttachedFile, \
|
|
SendingLimitation, Document, DocumentForwarded, AutomaticForwarding
|
|
from docbow_project.notification import NotificationFailure, notify
|
|
from decorator import no_delegate
|
|
from logger_adapter import get_logger
|
|
import timestamp
|
|
|
|
gettext_noop = lambda x: x
|
|
|
|
@login_required
|
|
def homepage(request):
|
|
return redirect('inbox')
|
|
|
|
class Row(object):
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
def listing(request, qs, template, view_name='', headers=None,
|
|
show_forwarding_status=False):
|
|
qs = qs.select_related(depth=2).order_by('-document__date')
|
|
paginator = Paginator(qs, settings.FILE_PER_PAGE)
|
|
page_number = request.GET.get('page', 1)
|
|
try:
|
|
page = paginator.page(page_number)
|
|
except PageNotAnInteger:
|
|
# If page is not an integer, deliver first page.
|
|
page = paginator.page(1)
|
|
except EmptyPage:
|
|
# If page is out of range (e.g. 9999), deliver last page of results.
|
|
page = paginator.page(paginator.num_pages)
|
|
file_list = []
|
|
if not headers:
|
|
headers = [ 'new_header', 'type_header', 'filename_header',
|
|
'sender_header', 'date_header', 'delete_column' ]
|
|
for item in page.object_list:
|
|
d = { 'href': reverse(view_name+'-message', args=(item.id,)) }
|
|
try:
|
|
d['seen'] = item.seen
|
|
except Mailbox.DoesNotExist:
|
|
d['seen'] = False
|
|
if view_name == 'outbox':
|
|
d['seen'] = True
|
|
d['comment'] = item.document.comment
|
|
values = {
|
|
gettext_noop('new_header'): ('X' if not d['seen'] else ' '),
|
|
gettext_noop('filename_header'): item.document.filenames(),
|
|
gettext_noop('type_header'): item.document.filetype.name,
|
|
gettext_noop('sender_header'): username(item.document.sender),
|
|
gettext_noop('comment_header'): item.document.comment,
|
|
gettext_noop('date_header'): item.document.date,
|
|
gettext_noop('delete_column'): 1,
|
|
}
|
|
cols = []
|
|
d['extra'] = []
|
|
for header in headers:
|
|
if header == 'comment_header':
|
|
d['extra'].append(values[header])
|
|
else:
|
|
cols.append(Row(id=header, caption=values[header]))
|
|
d['columns'] = cols
|
|
if show_forwarding_status:
|
|
d['forwards'] = item.document.document_forwarded_to.all()
|
|
file_list.append(d)
|
|
|
|
context = {
|
|
'page': page,
|
|
'view_name': view_name,
|
|
'file_list': file_list,
|
|
'headers': headers,
|
|
}
|
|
return render(request, template, context)
|
|
|
|
|
|
@login_required
|
|
def inbox(request):
|
|
qs = Mailbox.objects.filter(deleted=False, owner=request.user,
|
|
outbox=False)
|
|
headers = [ 'new_header', 'type_header', 'comment_header',
|
|
'filename_header', 'sender_header', 'date_header', 'delete_column'
|
|
]
|
|
specialize = request.session.get('ministre_or_parlementaire', None)
|
|
if specialize or request.user.groups.filter(Q(name='Parlementaires') | Q(name='Ministres')):
|
|
if not specialize:
|
|
request.session['ministre_or_parlementaire'] = True
|
|
headers = [ 'new_header', 'type_header', 'comment_header',
|
|
'filename_header', 'date_header', 'delete_column' ]
|
|
return listing(request, qs, 'docbow/inbox.html', view_name='inbox',
|
|
headers=headers, show_forwarding_status=can_forward(request))
|
|
|
|
@login_required
|
|
def outbox(request):
|
|
qs = Mailbox.objects.filter(deleted=False, owner=request.user, outbox=True)
|
|
return listing(request, qs, 'docbow/outbox.html', view_name='outbox',
|
|
headers=['type_header', 'comment_header', 'filename_header',
|
|
'date_header', 'delete_column'])
|
|
|
|
def send_mail_notifications(document, base_url,
|
|
subject_template='docbow/notify-email-subject.txt',
|
|
body_template='docbow/notify-email-body.txt',
|
|
html_body_template='docbow/notify-email-body.html',
|
|
logger=None):
|
|
if logger is None:
|
|
logger = logging.getLogger('docbow')
|
|
notify_emails = []
|
|
for user in document.to():
|
|
try:
|
|
validate_email(user.email)
|
|
notify_emails.append(user.email)
|
|
except ValidationError:
|
|
pass
|
|
location = reverse('inbox-by-document-message',
|
|
kwargs={'document_id': document.id})
|
|
url = base_url + location.lstrip('/')
|
|
context = {'document': document, 'url': url }
|
|
try:
|
|
mail = EmailMultiAlternatives(bcc=notify_emails,
|
|
subject=render_to_string(subject_template, context).strip(),
|
|
body=render_to_string(body_template, context))
|
|
mail.attach_alternative(render_to_string(html_body_template,
|
|
context), 'text/html')
|
|
mail.send(fail_silently=False)
|
|
logger.info('notification for document %(document_id)s \
|
|
to %(email_list)s', { 'document_id': document.id,
|
|
'email_list': ', '.join(notify_emails)})
|
|
return True
|
|
except (smtplib.SMTPException, socket.error), e:
|
|
logger.exception('unable to send one or more notifications for \
|
|
document %(document_id)s: %(error)s', { 'document_id': document.id, 'error': e })
|
|
return False
|
|
|
|
def user_mailing_list_names(user):
|
|
return user.mailing_lists.values_list('name', flat=True)
|
|
|
|
def get_file_form_kwargs(request):
|
|
limitation = SendingLimitation.objects.select_related().filter(mailing_list__members=request.user)
|
|
kwargs = {}
|
|
if limitation:
|
|
limitation = limitation[0]
|
|
if limitation.filetypes:
|
|
kwargs['filetype_qs'] = limitation.filetypes.all()
|
|
if limitation.lists:
|
|
kwargs['list_qs'] = limitation.lists.all()
|
|
kwargs['user_qs'] = User.objects.filter(mailing_lists=limitation.lists.all()).distinct()
|
|
return kwargs
|
|
|
|
def try_automatic_fowarding(document, logger, base_url):
|
|
'''Try to apply any automatic forwarding rule found matching the current
|
|
document.
|
|
'''
|
|
rules = AutomaticForwarding.objects.filter(filetypes=document.filetype,
|
|
originaly_to_user__in=list(document.to()))
|
|
if rules:
|
|
for rule in rules:
|
|
# choose a new sender
|
|
matching_recipients = rule.originaly_to_user.distinct() & document.to()
|
|
first_matching_recipient = matching_recipients[0]
|
|
try:
|
|
notification_count = forward_document(document, first_matching_recipient,
|
|
rule.forward_to_user.all(),
|
|
rule.forward_to_list.all(), logger, base_url,
|
|
automatic=True)
|
|
except Exception:
|
|
logger.exception('Unhandled exception during '
|
|
'application of automatic forwarding')
|
|
else:
|
|
if not notification_count:
|
|
logger.error('automatic forwarding failed '
|
|
'because we were unable to send '
|
|
'some notifications')
|
|
|
|
@login_required
|
|
def send_file(request):
|
|
logger = get_logger(request)
|
|
if request.method == 'POST':
|
|
if 'send' not in request.POST:
|
|
return redirect('outbox')
|
|
form = FileForm(request.POST, request.FILES, user=request.user,
|
|
**get_file_form_kwargs(request))
|
|
if form.is_valid():
|
|
with transaction.commit_on_success():
|
|
new_send = form.save(commit=False)
|
|
new_send.sender = request.user
|
|
to_user = []
|
|
to_list = []
|
|
for recipient in form.cleaned_data['recipients']:
|
|
if recipient.startswith('list-'):
|
|
i = recipient.split('-')[1]
|
|
to_list.append(int(i))
|
|
elif recipient.startswith('user-'):
|
|
i = recipient.split('-')[1]
|
|
to_user.append(int(i))
|
|
new_send.save()
|
|
form.save_attachments()
|
|
new_send.to_user = to_user
|
|
new_send.to_list = to_list
|
|
notification_count = new_send.post()
|
|
base_url = request.build_absolute_uri('/')
|
|
if not send_mail_notifications(new_send, base_url, logger=logger):
|
|
transaction.rollback()
|
|
messages.error(request, _('Document send failed because '
|
|
'some notifications could not be sent, administrators '
|
|
'have been informed.'))
|
|
else:
|
|
blob = new_send.timestamp_blob()
|
|
tst = timestamp.timestamp_json(blob)
|
|
logger.info('sent %s, timestamp %s' % (new_send, tst))
|
|
msg = ungettext(
|
|
'New document sent to %d recipient.',
|
|
'New document sent to %d recipients.',
|
|
notification_count)
|
|
messages.info(request, msg % notification_count)
|
|
try_automatic_fowarding(new_send, logger, base_url)
|
|
return redirect('outbox')
|
|
else:
|
|
form = FileForm(user=request.user, **get_file_form_kwargs(request))
|
|
return render(request, 'docbow/send_file.html',
|
|
{'form': form, 'view_name': 'send-file'})
|
|
|
|
def upload(request, attached_file):
|
|
response = HttpResponse(attached_file.content.chunks(), mimetype='application/octet-stream')
|
|
response['Content-disposition'] = 'attachment'
|
|
return response
|
|
|
|
@login_required
|
|
@transaction.commit_on_success
|
|
def message_attached_file(request, mailbox_id, attached_file):
|
|
'''
|
|
Download attached files, verify that the user has access to the document
|
|
before, otherwise return 404.
|
|
'''
|
|
logger = get_logger(request)
|
|
mailbox = get_object_or_404(Mailbox.objects.select_related(),
|
|
id=mailbox_id, owner=request.user)
|
|
document = mailbox.document
|
|
try:
|
|
attached_file = document.attached_files.get(pk=attached_file)
|
|
mailbox.seen = True
|
|
mailbox.save()
|
|
document_id = document.id
|
|
logger.info(_('download attached file %(attached_file)s from sent document %(document)s named %(filename)s')
|
|
% { 'document': document_id,
|
|
'attached_file': attached_file.id,
|
|
'filename': attached_file.filename() })
|
|
return upload(request, attached_file)
|
|
except AttachedFile.DoesNotExist:
|
|
raise Http404
|
|
|
|
def can_forward(request):
|
|
'''Is the current user allowed to forward a Document ?'''
|
|
return request.user.username == 'Greffe' or \
|
|
bool(request.user.mailing_lists.filter(name__in=('Greffe',))) or \
|
|
bool(request.user.groups.filter(name__in=('Administrateurs',)))
|
|
|
|
def user_who_can_forward_filter():
|
|
'''Return the list of users who have access to the forwarding feature'''
|
|
return User.objects.filter(Q(username='Greffe') |
|
|
Q(mailing_lists__name='Greffe') |
|
|
Q(groups__name='Administrateurs'))
|
|
|
|
def mark_document_read(document, logger=None):
|
|
'''
|
|
Mark all mailboxes object for this document of user who can forward as
|
|
read.
|
|
'''
|
|
users = user_who_can_forward_filter()
|
|
mboxes = Mailbox.objects.filter(owner__in=users, document=document)
|
|
if not logger:
|
|
logger = logging.getLogger('docbow')
|
|
logger.info(_('Marking mailboxes %s as read because document %s was forwarded'),
|
|
'. '.join([str(mbox.id) for mbox in mboxes]), document.id)
|
|
mboxes.update(seen=True)
|
|
|
|
def form_to_user_and_list(form):
|
|
to_user = []
|
|
to_list = []
|
|
for recipient in form.cleaned_data['recipients']:
|
|
if recipient.startswith('list-'):
|
|
i = recipient.split('-')[1]
|
|
to_list.append(int(i))
|
|
elif recipient.startswith('user-'):
|
|
i = recipient.split('-')[1]
|
|
to_user.append(int(i))
|
|
return to_user, to_list
|
|
|
|
def forward_document(document, sender, to_user, to_list, logger, base_url,
|
|
automatic=False):
|
|
new_document = Document()
|
|
new_document.sender = sender
|
|
new_document.filetype = document.filetype
|
|
new_document.comment = document.comment
|
|
new_document.save()
|
|
new_document.to_user = to_user
|
|
new_document.to_list = to_list
|
|
new_document.save()
|
|
for attached_file in document.attached_files.all():
|
|
AttachedFile.objects.get_or_create(
|
|
name=attached_file.name,
|
|
content=attached_file.content,
|
|
document=new_document)
|
|
notification_count = new_document.post()
|
|
DocumentForwarded.objects.get_or_create(from_document=document,
|
|
to_document=new_document, automatic=automatic)
|
|
if not send_mail_notifications(new_document, base_url, logger=logger):
|
|
transaction.rollback()
|
|
return False
|
|
else:
|
|
blob = new_document.timestamp_blob()
|
|
tst = timestamp.timestamp_json(blob)
|
|
is_automatic = 'automatically' if automatic else ''
|
|
logger.info('document %s forwarded %s as document %s, timestamp %s' %
|
|
(document, is_automatic, new_document, tst))
|
|
mark_document_read(document, logger=logger)
|
|
return notification_count
|
|
|
|
@login_required
|
|
def message(request, mailbox_id, back='inbox'):
|
|
mailbox_content = get_object_or_404(Mailbox, pk=mailbox_id,
|
|
owner=request.user, outbox=back!='inbox')
|
|
document = mailbox_content.document
|
|
logger = get_logger(request)
|
|
if back == 'inbox':
|
|
back_pair = (reverse('inbox'), gettext_noop('back to inbox'))
|
|
else:
|
|
back_pair = (reverse('outbox'), gettext_noop('back to outbox'))
|
|
logger.info(_('view details of document %(document)s')
|
|
% { 'user': request.user, 'document': document.pk })
|
|
ctx = { 'document': document,
|
|
'view_name': back, 'back': back_pair }
|
|
if back == 'inbox' and can_forward(request):
|
|
if request.method == 'POST':
|
|
form = ForwardingForm(request.POST, user=request.user)
|
|
if form.is_valid():
|
|
with transaction.commit_on_success():
|
|
to_user, to_list = form_to_user_and_list(form)
|
|
base_url = request.build_absolute_uri('/')
|
|
notification_count = forward_document(document, request.user, to_user,
|
|
to_list, logger, base_url)
|
|
if notification_count:
|
|
msg = ungettext(
|
|
'Forwarded document sent to %d recipient.',
|
|
'Forwarded document sent to %d recipients.',
|
|
notification_count)
|
|
messages.info(request, msg % notification_count)
|
|
return HttpResponseRedirect('')
|
|
else:
|
|
messages.error(request, _('Document forwarding failed because '
|
|
'some notifications could not be sent, administrators '
|
|
'have been informed.'))
|
|
else:
|
|
form = ForwardingForm(user=request.user)
|
|
ctx['form'] = form
|
|
return render(request, 'docbow/message.html', ctx)
|
|
|
|
|
|
__HELP_CONTENT = None
|
|
__HELP_URL = 'http://wiki.entrouvert.org/Docbow'
|
|
__HELP_PREFIX = 'http://wiki.entrouvert.org'
|
|
|
|
def get_help_content():
|
|
global __HELP_CONTENT
|
|
|
|
if __HELP_CONTENT is None:
|
|
doc = urllib.urlopen(__HELP_URL).read()
|
|
parsed_doc = BeautifulSoup(doc)
|
|
page = parsed_doc.findAll(True, id='page')[0]
|
|
for img in page.findAll('img'):
|
|
name = img['src'].split('target=')[1]
|
|
img['src'] = __HELP_PREFIX + img['src']
|
|
# pull
|
|
cache_dir = os.path.join(settings.STATIC_ROOT, 'cache')
|
|
if not os.path.exists(cache_dir):
|
|
os.mkdir(cache_dir)
|
|
cached_name = os.path.join(cache_dir, name)
|
|
open(cached_name, 'w').write(urllib.urlopen(img['src']).read())
|
|
img['src'] = '/static/cache/%s' % name
|
|
for t in page.findAll('span'):
|
|
if len(t.contents) == 0:
|
|
t.extract()
|
|
for t in page.findAll(id='pageinfo'):
|
|
t.extract()
|
|
for t in page.findAll('h1'):
|
|
t.name = 'h4'
|
|
for t in page.findAll('h2'):
|
|
t.name = 'h5'
|
|
for t in page.findAll('p', { 'class': 'table-of-contents-heading' }):
|
|
t.contents[0].replaceWith('<h4>%s</h4>' % t.contents[0])
|
|
__HELP_CONTENT = unicode(page)
|
|
return __HELP_CONTENT
|
|
|
|
@login_required
|
|
def help(request):
|
|
return render(request, 'docbow/help.html', { 'view_name': 'help', 'content': get_help_content() })
|
|
|
|
def contact(request, template='docbow/contact.html',
|
|
form_class=AnonymousContactForm):
|
|
logger = get_logger(request)
|
|
user = request.user
|
|
if user.is_authenticated():
|
|
template = 'docbow/contact_user.html'
|
|
form_class = ContactForm
|
|
contacts = User.objects.filter(groups__name__in=settings.CONTACT_GROUPS)
|
|
if not bool(contacts):
|
|
messages.error(request, _('No administror group is configured to receive contact mails'))
|
|
return redirect('inbox')
|
|
if request.method == 'POST':
|
|
form = form_class(request.POST)
|
|
if form.is_valid():
|
|
cleaned_data = form.cleaned_data
|
|
to = [ contact.email for contact in contacts ]
|
|
subject = settings.CONTACT_SUBJECT_PREFIX + cleaned_data['subject']
|
|
message = cleaned_data['message']
|
|
if user.is_authenticated():
|
|
reply_to = user.email
|
|
body = _('Message from %(name)s <%(email)s>') % { 'name':
|
|
user.get_full_name(), 'email': reply_to } + '\n\n%s' % message
|
|
else:
|
|
reply_to = cleaned_data['email']
|
|
body = _('Message from %(name)s <%(email)s>') % cleaned_data
|
|
if cleaned_data.get('phone_number'):
|
|
body += _('\nPhone number: %s') % cleaned_data['phone_number']
|
|
body += '\n\n%s' % message
|
|
try:
|
|
EmailMessage(to=to, subject=subject, body=body,
|
|
headers={'Reply-To': reply_to}).send()
|
|
|
|
messages.info(request, _('Your message was sent to %d administrators') % len(to))
|
|
logger.info('new contact mail sent: %s', cleaned_data)
|
|
except smtplib.SMTPException, socket.error:
|
|
logger.exception('unable to send the contact mail: %s' % cleaned_data)
|
|
return redirect('inbox')
|
|
else:
|
|
form = form_class()
|
|
return render(request, template, { 'form': form, 'view_name': 'contact' })
|
|
|
|
def logout(request):
|
|
auth.logout(request)
|
|
return redirect('inbox')
|
|
|
|
@login_required
|
|
def delete(request, mailbox_id, back='inbox'):
|
|
'''Remove a document from the inbox'''
|
|
logger = get_logger(request)
|
|
page = request.GET.get('page', 1)
|
|
viewname = back + '-message-delete'
|
|
back_pair = ('%s?page=%s' % (reverse(back), page),
|
|
gettext_noop('back to %s' % back))
|
|
m = get_object_or_404(Mailbox, pk=mailbox_id)
|
|
if request.method == 'GET':
|
|
return render(request, 'docbow/delete.html', { 'document': m.document, 'back': back_pair, 'view_name': viewname })
|
|
else:
|
|
m.deleted=True
|
|
m.save()
|
|
logger.info('mailbox entry %(mailbox_id)s marked as deleted', {
|
|
'mailbox_id': mailbox_id })
|
|
return redirect(back_pair[0])
|
|
|
|
def get_free_delegation_number(user):
|
|
'''Allocate a new delegation username'''
|
|
for i in count(1):
|
|
delegate_username = user.username + '-%d' % i
|
|
try:
|
|
User.objects.get(username=delegate_username)
|
|
except User.DoesNotExist:
|
|
return delegate_username
|
|
|
|
|
|
def make_password_reset_url(request, user):
|
|
uid = int_to_base36(user.id)
|
|
token = default_token_generator.make_token(user)
|
|
return request.build_absolute_uri(
|
|
reverse('auth_password_reset_confirm',
|
|
kwargs={'uidb36': uid, 'token': token}))
|
|
|
|
@login_required
|
|
@no_delegate
|
|
def delegate(request, domain_override=None,):
|
|
logger = get_logger(request)
|
|
delegations = Delegation.objects \
|
|
.select_related().filter(by=request.user)
|
|
form = DelegationForm()
|
|
if request.method == 'POST':
|
|
if 'create' in request.POST:
|
|
form = DelegationForm(request.POST)
|
|
if form.is_valid():
|
|
try:
|
|
with transaction.commit_on_success():
|
|
delegate_username = get_free_delegation_number(request.user)
|
|
delegate_user = User(username=delegate_username,
|
|
**form.cleaned_data)
|
|
delegate_user.save()
|
|
Delegation.objects.get_or_create(by=request.user,
|
|
to=delegate_user, guest_delegate=True)
|
|
ctx = {
|
|
'user': request.user,
|
|
'delegate': delegate_user,
|
|
'password_reset_link':
|
|
make_password_reset_url(request, delegate_user),
|
|
}
|
|
notify(request, 'new-delegation-mail', ctx=ctx, to=form.cleaned_data['email'],
|
|
reply_to=request.user.email)
|
|
except NotificationFailure:
|
|
messages.error(request, _('The delegation creation failed when notifying your new delegate, could you check the email you provided ?'))
|
|
except:
|
|
raise
|
|
logger.exception('unable to create a delegation')
|
|
messages.error(request, _('The delegation creation failed, an administrator has been notified.'))
|
|
else:
|
|
messages.info(request, _('New delegation to user %s created.') % delegate_user.username)
|
|
messages.info(request, _('A notification was sent to %s about this new delegation') % delegate_user.email)
|
|
|
|
return redirect('delegate')
|
|
else:
|
|
for delegation in delegations:
|
|
if 'delete-%s.x' % delegation.to.username in request.POST:
|
|
with transaction.commit_on_success():
|
|
delegate_user = delegation.to
|
|
delegation.delete()
|
|
if delegation.guest_delegate:
|
|
delegate_user.delete()
|
|
messages.info(request, _('Delegation %s supressed') % delegate_user)
|
|
ctx = {
|
|
'user': request.user,
|
|
'delegate': delegate_user,
|
|
}
|
|
try:
|
|
notify(request, 'delete-delegation-mail',
|
|
ctx=ctx, to=delegate_user.email,
|
|
reply_to=request.user.email,
|
|
with_html=False)
|
|
messages.info(request, _('%s has been notified.') % delegate_user.email)
|
|
except NotificationFailure:
|
|
messages.warning(request, _('Unable to notify %s of \
|
|
the delegation suppression, you should do it yourself') % delegate_user.email)
|
|
return redirect('delegate')
|
|
ctx = {
|
|
'delegations': delegations,
|
|
'form': form,
|
|
}
|
|
return render(request, 'docbow/delegate.html', ctx)
|
|
|
|
|
|
@login_required
|
|
def inbox_by_document(request, document_id):
|
|
mailbox = get_object_or_404(Mailbox, owner=request.user, outbox=False,
|
|
document=document_id)
|
|
return redirect('inbox-message', mailbox.id, permanent=True)
|