mirror of
https://gitea.mueller.network/extern/django-helpdesk.git
synced 2024-11-22 07:53:19 +01:00
8dbd54ac16
We accomplish this by attching files to out-bound mail diffrently depending on the versino of Python in effect. In Py2 we can read the files ourseles and the standard library will still be able to use the text we pass as if it were bytes. Under Py3, however, email.message will complain if it doesn't get to decode the bytes itself, so instead of attaching the contents directly we just pass the path to the file as a string instead. Unfortunately, Django 1.8 does not work with this Python 3 approach, due to its not yet having reverted to the newly improved standard library's mail-message implementation, and thus requiring us to know more about the character-encoding/mimetype of the attachment than I've been able to gather cleanly by this point.
308 lines
10 KiB
Python
308 lines
10 KiB
Python
"""
|
|
django-helpdesk - A Django powered ticket tracker for small enterprise.
|
|
|
|
(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details.
|
|
|
|
lib.py - Common functions (eg multipart e-mail)
|
|
"""
|
|
|
|
import logging
|
|
import mimetypes
|
|
import os
|
|
|
|
try:
|
|
from base64 import urlsafe_b64encode as b64encode
|
|
except ImportError:
|
|
from base64 import encodestring as b64encode
|
|
try:
|
|
from base64 import urlsafe_b64decode as b64decode
|
|
except ImportError:
|
|
from base64 import decodestring as b64decode
|
|
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from django.utils import six
|
|
from django.utils.encoding import smart_text
|
|
from django.utils.safestring import mark_safe
|
|
|
|
from helpdesk.models import Attachment, EmailTemplate
|
|
|
|
logger = logging.getLogger('helpdesk')
|
|
|
|
|
|
def send_templated_mail(template_name,
|
|
context,
|
|
recipients,
|
|
sender=None,
|
|
bcc=None,
|
|
fail_silently=False,
|
|
files=None):
|
|
"""
|
|
send_templated_mail() is a wrapper around Django's e-mail routines that
|
|
allows us to easily send multipart (text/plain & text/html) e-mails using
|
|
templates that are stored in the database. This lets the admin provide
|
|
both a text and a HTML template for each message.
|
|
|
|
template_name is the slug of the template to use for this message (see
|
|
models.EmailTemplate)
|
|
|
|
context is a dictionary to be used when rendering the template
|
|
|
|
recipients can be either a string, eg 'a@b.com', or a list of strings.
|
|
|
|
sender should contain a string, eg 'My Site <me@z.com>'. If you leave it
|
|
blank, it'll use settings.DEFAULT_FROM_EMAIL as a fallback.
|
|
|
|
bcc is an optional list of addresses that will receive this message as a
|
|
blind carbon copy.
|
|
|
|
fail_silently is passed to Django's mail routine. Set to 'True' to ignore
|
|
any errors at send time.
|
|
|
|
files can be a list of tuples. Each tuple should be a filename to attach,
|
|
along with the File objects to be read. files can be blank.
|
|
|
|
"""
|
|
from django.core.mail import EmailMultiAlternatives
|
|
from django.template import engines
|
|
from_string = engines['django'].from_string
|
|
|
|
from helpdesk.models import EmailTemplate
|
|
from helpdesk.settings import HELPDESK_EMAIL_SUBJECT_TEMPLATE, \
|
|
HELPDESK_EMAIL_FALLBACK_LOCALE
|
|
|
|
locale = context['queue'].get('locale') or HELPDESK_EMAIL_FALLBACK_LOCALE
|
|
|
|
try:
|
|
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale=locale)
|
|
except EmailTemplate.DoesNotExist:
|
|
try:
|
|
t = EmailTemplate.objects.get(template_name__iexact=template_name, locale__isnull=True)
|
|
except EmailTemplate.DoesNotExist:
|
|
logger.warning('template "%s" does not exist, no mail sent', template_name)
|
|
return # just ignore if template doesn't exist
|
|
|
|
subject_part = from_string(
|
|
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {"subject": t.subject}
|
|
).render(context).replace('\n', '').replace('\r', '')
|
|
|
|
footer_file = os.path.join('helpdesk', locale, 'email_text_footer.txt')
|
|
|
|
text_part = from_string(
|
|
"%s{%% include '%s' %%}" % (t.plain_text, footer_file)
|
|
).render(context)
|
|
|
|
email_html_base_file = os.path.join('helpdesk', locale, 'email_html_base.html')
|
|
# keep new lines in html emails
|
|
if 'comment' in context:
|
|
context['comment'] = mark_safe(context['comment'].replace('\r\n', '<br>'))
|
|
|
|
html_part = from_string(
|
|
"{%% extends '%s' %%}{%% block title %%}"
|
|
"%s"
|
|
"{%% endblock %%}{%% block content %%}%s{%% endblock %%}" %
|
|
(email_html_base_file, t.heading, t.html)
|
|
).render(context)
|
|
|
|
subject_part = from_string(
|
|
HELPDESK_EMAIL_SUBJECT_TEMPLATE % {
|
|
"subject": t.subject,
|
|
}).render(context)
|
|
|
|
if isinstance(recipients, str):
|
|
if recipients.find(','):
|
|
recipients = recipients.split(',')
|
|
elif type(recipients) != list:
|
|
recipients = [recipients]
|
|
|
|
msg = EmailMultiAlternatives(subject_part, text_part,
|
|
sender or settings.DEFAULT_FROM_EMAIL,
|
|
recipients, bcc=bcc)
|
|
msg.attach_alternative(html_part, "text/html")
|
|
|
|
if files:
|
|
for filename, filefield in files:
|
|
if six.PY3:
|
|
msg.attach_file(filefield.path)
|
|
else:
|
|
with open(filefield.path) as attachedfile:
|
|
msg.attach(filename, attachedfile.read())
|
|
|
|
return msg.send(fail_silently)
|
|
|
|
|
|
def query_to_dict(results, descriptions):
|
|
"""
|
|
Replacement method for cursor.dictfetchall() as that method no longer
|
|
exists in psycopg2, and I'm guessing in other backends too.
|
|
|
|
Converts the results of a raw SQL query into a list of dictionaries, suitable
|
|
for use in templates etc.
|
|
"""
|
|
|
|
output = []
|
|
for data in results:
|
|
row = {}
|
|
i = 0
|
|
for column in descriptions:
|
|
row[column[0]] = data[i]
|
|
i += 1
|
|
|
|
output.append(row)
|
|
return output
|
|
|
|
|
|
def apply_query(queryset, params):
|
|
"""
|
|
Apply a dict-based set of filters & parameters to a queryset.
|
|
|
|
queryset is a Django queryset, eg MyModel.objects.all() or
|
|
MyModel.objects.filter(user=request.user)
|
|
|
|
params is a dictionary that contains the following:
|
|
filtering: A dict of Django ORM filters, eg:
|
|
{'user__id__in': [1, 3, 103], 'title__contains': 'foo'}
|
|
|
|
search_string: A freetext search string
|
|
|
|
sorting: The name of the column to sort by
|
|
"""
|
|
for key in params['filtering'].keys():
|
|
filter = {key: params['filtering'][key]}
|
|
queryset = queryset.filter(**filter)
|
|
|
|
search = params.get('search_string', None)
|
|
if search:
|
|
qset = (
|
|
Q(title__icontains=search) |
|
|
Q(description__icontains=search) |
|
|
Q(resolution__icontains=search) |
|
|
Q(submitter_email__icontains=search)
|
|
)
|
|
|
|
queryset = queryset.filter(qset)
|
|
|
|
sorting = params.get('sorting', None)
|
|
if sorting:
|
|
sortreverse = params.get('sortreverse', None)
|
|
if sortreverse:
|
|
sorting = "-%s" % sorting
|
|
queryset = queryset.order_by(sorting)
|
|
|
|
return queryset
|
|
|
|
|
|
def safe_template_context(ticket):
|
|
"""
|
|
Return a dictionary that can be used as a template context to render
|
|
comments and other details with ticket or queue parameters. Note that
|
|
we don't just provide the Ticket & Queue objects to the template as
|
|
they could reveal confidential information. Just imagine these two options:
|
|
* {{ ticket.queue.email_box_password }}
|
|
* {{ ticket.assigned_to.password }}
|
|
|
|
Ouch!
|
|
|
|
The downside to this is that if we make changes to the model, we will also
|
|
have to update this code. Perhaps we can find a better way in the future.
|
|
"""
|
|
|
|
context = {
|
|
'queue': {},
|
|
'ticket': {}
|
|
}
|
|
queue = ticket.queue
|
|
|
|
for field in ('title', 'slug', 'email_address', 'from_address', 'locale'):
|
|
attr = getattr(queue, field, None)
|
|
if callable(attr):
|
|
context['queue'][field] = attr()
|
|
else:
|
|
context['queue'][field] = attr
|
|
|
|
for field in ('title', 'created', 'modified', 'submitter_email',
|
|
'status', 'get_status_display', 'on_hold', 'description',
|
|
'resolution', 'priority', 'get_priority_display',
|
|
'last_escalation', 'ticket', 'ticket_for_url',
|
|
'get_status', 'ticket_url', 'staff_url', '_get_assigned_to'
|
|
):
|
|
attr = getattr(ticket, field, None)
|
|
if callable(attr):
|
|
context['ticket'][field] = '%s' % attr()
|
|
else:
|
|
context['ticket'][field] = attr
|
|
|
|
context['ticket']['queue'] = context['queue']
|
|
context['ticket']['assigned_to'] = context['ticket']['_get_assigned_to']
|
|
|
|
return context
|
|
|
|
|
|
def text_is_spam(text, request):
|
|
# Based on a blog post by 'sciyoshi':
|
|
# http://sciyoshi.com/blog/2008/aug/27/using-akismet-djangos-new-comments-framework/
|
|
# This will return 'True' is the given text is deemed to be spam, or
|
|
# False if it is not spam. If it cannot be checked for some reason, we
|
|
# assume it isn't spam.
|
|
from django.contrib.sites.models import Site
|
|
try:
|
|
from helpdesk.akismet import Akismet
|
|
except:
|
|
return False
|
|
try:
|
|
site = Site.objects.get_current()
|
|
except:
|
|
site = Site(domain='configure-django-sites.com')
|
|
|
|
ak = Akismet(
|
|
blog_url='http://%s/' % site.domain,
|
|
agent='django-helpdesk',
|
|
)
|
|
|
|
if hasattr(settings, 'TYPEPAD_ANTISPAM_API_KEY'):
|
|
ak.setAPIKey(key=settings.TYPEPAD_ANTISPAM_API_KEY)
|
|
ak.baseurl = 'api.antispam.typepad.com/1.1/'
|
|
elif hasattr(settings, 'AKISMET_API_KEY'):
|
|
ak.setAPIKey(key=settings.AKISMET_API_KEY)
|
|
else:
|
|
return False
|
|
|
|
if ak.verify_key():
|
|
ak_data = {
|
|
'user_ip': request.META.get('REMOTE_ADDR', '127.0.0.1'),
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
'referrer': request.META.get('HTTP_REFERER', ''),
|
|
'comment_type': 'comment',
|
|
'comment_author': '',
|
|
}
|
|
|
|
return ak.comment_check(smart_text(text), data=ak_data)
|
|
|
|
return False
|
|
|
|
|
|
def process_attachments(followup, attached_files):
|
|
max_email_attachment_size = getattr(settings, 'MAX_EMAIL_ATTACHMENT_SIZE', 512000)
|
|
attachments = []
|
|
|
|
for attached in attached_files:
|
|
if attached.size:
|
|
filename = smart_text(attached.name)
|
|
att = Attachment(
|
|
followup=followup,
|
|
file=attached,
|
|
filename=filename,
|
|
mime_type=attached.content_type or
|
|
mimetypes.guess_type(filename, strict=False)[0] or
|
|
'application/octet-stream',
|
|
size=attached.size,
|
|
)
|
|
att.save()
|
|
|
|
if attached.size < max_email_attachment_size:
|
|
# Only files smaller than 512kb (or as defined in
|
|
# settings.MAX_EMAIL_ATTACHMENT_SIZE) are sent via email.
|
|
attachments.append([filename, att.file])
|
|
|
|
return attachments
|