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('

%s

' % 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)