From d76fa2c71ece026e6d00d532b53bd2ecc269e6b5 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 09:32:04 +0100 Subject: [PATCH 01/45] PY3 support and msgNum error solved in process_queue method --- helpdesk/management/commands/get_email.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index e5b40c41..d2f3d70a 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -214,9 +214,9 @@ def process_queue(q, logger): ticket = ticket_from_message(message=data[0][1], queue=q, logger=logger) if ticket: server.store(num, '+FLAGS', '\\Deleted') - logger.info("Successfully processed message %s, deleted from IMAP server" % str(msgNum)) + logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(msgNum)) + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) server.expunge() server.close() @@ -271,7 +271,10 @@ def decode_mail_headers(string): def ticket_from_message(message, queue, logger): # 'message' must be an RFC822 formatted message. msg = message - message = email.message_from_string(msg) + if six.PY2: + message = email.message_from_string(msg) + elif six.PY3: + message = email.message_from_bytes(msg) subject = message.get('subject', _('Created from e-mail')) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: From 4c97ecd0aebe6b6989dec018236f812c9b3a00d6 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 15:14:11 +0100 Subject: [PATCH 02/45] PY3 get_email support in method ticket_from_message (if message isinstance bytes use email.email_from_bytes(msg) else ..from_string(msg)) --- helpdesk/management/commands/get_email.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index d2f3d70a..3c85a8ae 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -274,7 +274,13 @@ def ticket_from_message(message, queue, logger): if six.PY2: message = email.message_from_string(msg) elif six.PY3: - message = email.message_from_bytes(msg) + print(type(msg)) + print(msg) + print(isinstance(msg, bytes)) + if isinstance(msg, bytes): + message = email.message_from_bytes(msg) + else: + message = email.message_from_string(msg) subject = message.get('subject', _('Created from e-mail')) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: From 5998faa6a59c9c1f9b3ed00969fa68adc80f4976 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 15:17:14 +0100 Subject: [PATCH 03/45] PY3 get_email support in method ticket_from_message (if message isinstance bytes use email.email_from_bytes(msg) else ..from_string(msg)) ... clear --- helpdesk/management/commands/get_email.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 3c85a8ae..48b765b0 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -274,9 +274,6 @@ def ticket_from_message(message, queue, logger): if six.PY2: message = email.message_from_string(msg) elif six.PY3: - print(type(msg)) - print(msg) - print(isinstance(msg, bytes)) if isinstance(msg, bytes): message = email.message_from_bytes(msg) else: From 905910911de5a783e9acef9d13541f606a1e3de6 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 09:32:04 +0100 Subject: [PATCH 04/45] PY3 support and msgNum error solved in process_queue method --- helpdesk/management/commands/get_email.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 0bf4c0a3..b1809afa 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -205,9 +205,9 @@ def process_queue(q, logger): ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) if ticket: server.store(num, '+FLAGS', '\\Deleted') - logger.info("Successfully processed message %s, deleted from IMAP server" % num) + logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) server.expunge() server.close() @@ -264,7 +264,11 @@ def decode_mail_headers(string): def ticket_from_message(message, queue, logger): # 'message' must be an RFC822 formatted message. - message = email.message_from_string(message) + msg = message + if six.PY2: + message = email.message_from_string(msg) + elif six.PY3: + message = email.message_from_bytes(msg) subject = message.get('subject', _('Created from e-mail')) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: From 550ca8941565ed0625de94d6f8247d10964aaa32 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 15:14:11 +0100 Subject: [PATCH 05/45] PY3 get_email support in method ticket_from_message (if message isinstance bytes use email.email_from_bytes(msg) else ..from_string(msg)) --- helpdesk/management/commands/get_email.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index b1809afa..d9a1969f 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -268,7 +268,13 @@ def ticket_from_message(message, queue, logger): if six.PY2: message = email.message_from_string(msg) elif six.PY3: - message = email.message_from_bytes(msg) + print(type(msg)) + print(msg) + print(isinstance(msg, bytes)) + if isinstance(msg, bytes): + message = email.message_from_bytes(msg) + else: + message = email.message_from_string(msg) subject = message.get('subject', _('Created from e-mail')) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: From c11e360f505010acbe29a4513d4d6295657ae38f Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 15:17:14 +0100 Subject: [PATCH 06/45] PY3 get_email support in method ticket_from_message (if message isinstance bytes use email.email_from_bytes(msg) else ..from_string(msg)) ... clear --- helpdesk/management/commands/get_email.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index d9a1969f..8d332dc6 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -268,9 +268,6 @@ def ticket_from_message(message, queue, logger): if six.PY2: message = email.message_from_string(msg) elif six.PY3: - print(type(msg)) - print(msg) - print(isinstance(msg, bytes)) if isinstance(msg, bytes): message = email.message_from_bytes(msg) else: From 04ec40bbc8b26c932513f2b5612f933a3a87d59c Mon Sep 17 00:00:00 2001 From: Pawel M Date: Mon, 12 Dec 2016 10:31:29 +0100 Subject: [PATCH 07/45] merged with upstream --- helpdesk/management/commands/get_email.py | 15 +- .../management/commands/get_email.py.orig | 448 ++++++++++++++++++ 2 files changed, 455 insertions(+), 8 deletions(-) create mode 100644 helpdesk/management/commands/get_email.py.orig diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 8d332dc6..47dd5e9b 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -264,14 +264,13 @@ def decode_mail_headers(string): def ticket_from_message(message, queue, logger): # 'message' must be an RFC822 formatted message. - msg = message - if six.PY2: - message = email.message_from_string(msg) - elif six.PY3: - if isinstance(msg, bytes): - message = email.message_from_bytes(msg) - else: - message = email.message_from_string(msg) +# if six.PY2: + message = email.message_from_string(message) +# elif six.PY3: +# if isinstance(msg, bytes): +# message = email.message_from_bytes(message) +# else: +# message = email.message_from_string(message) subject = message.get('subject', _('Created from e-mail')) subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) for affix in STRIPPED_SUBJECT_STRINGS: diff --git a/helpdesk/management/commands/get_email.py.orig b/helpdesk/management/commands/get_email.py.orig new file mode 100644 index 00000000..b662760f --- /dev/null +++ b/helpdesk/management/commands/get_email.py.orig @@ -0,0 +1,448 @@ +#!/usr/bin/python +""" +Jutda Helpdesk - A Django powered ticket tracker for small enterprise. + +(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. + +scripts/get_email.py - Designed to be run from cron, this script checks the + POP and IMAP boxes, or a local mailbox directory, + defined for the queues within a + helpdesk, creating tickets from the new messages (or + adding to existing tickets if needed) +""" + +from datetime import timedelta +import email +import imaplib +import mimetypes +from os import listdir, unlink +from os.path import isfile, join +import poplib +import re +import socket +from time import ctime + +from email_reply_parser import EmailReplyParser + +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management.base import BaseCommand +from django.db.models import Q +from django.utils.translation import ugettext as _ +from django.utils import encoding, six, timezone + +from helpdesk import settings +from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments +from helpdesk.models import Queue, Ticket, FollowUp, IgnoreEmail + +import logging + + +STRIPPED_SUBJECT_STRINGS = [ + "Re: ", + "Fw: ", + "RE: ", + "FW: ", + "Automatic reply: ", +] + + +class Command(BaseCommand): + + def __init__(self): + BaseCommand.__init__(self) + + help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ + 'from a local mailbox directory as required, feeding them into the helpdesk.' + + def add_arguments(self, parser): + parser.add_argument( + '--quiet', + action='store_true', + dest='quiet', + default=False, + help='Hide details about each queue/message as they are processed', + ) + + def handle(self, *args, **options): + quiet = options.get('quiet', False) + process_email(quiet=quiet) + + +def process_email(quiet=False): + for q in Queue.objects.filter( + email_box_type__isnull=False, + allow_email_submission=True): + + logger = logging.getLogger('django.helpdesk.queue.' + q.slug) + if not q.logging_type or q.logging_type == 'none': + logging.disable(logging.CRITICAL) # disable all messages + elif q.logging_type == 'info': + logger.setLevel(logging.INFO) + elif q.logging_type == 'warn': + logger.setLevel(logging.WARN) + elif q.logging_type == 'error': + logger.setLevel(logging.ERROR) + elif q.logging_type == 'crit': + logger.setLevel(logging.CRITICAL) + elif q.logging_type == 'debug': + logger.setLevel(logging.DEBUG) + if quiet: + logger.propagate = False # do not propagate to root logger that would log to console + logdir = q.logging_dir or '/var/log/helpdesk/' + handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log')) + logger.addHandler(handler) + + if not q.email_box_last_check: + q.email_box_last_check = timezone.now() - timedelta(minutes=30) + + queue_time_delta = timedelta(minutes=q.email_box_interval or 0) + + if (q.email_box_last_check + queue_time_delta) < timezone.now(): + process_queue(q, logger=logger) + q.email_box_last_check = timezone.now() + q.save() + + +def process_queue(q, logger): + logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) + + if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: + try: + import socks + except ImportError: + no_socks_msg = "Queue has been configured with proxy settings, " \ + "but no socks library was installed. Try to " \ + "install PySocks via PyPI." + logger.error(no_socks_msg) + raise ImportError(no_socks_msg) + + proxy_type = { + 'socks4': socks.SOCKS4, + 'socks5': socks.SOCKS5, + }.get(q.socks_proxy_type) + + socks.set_default_proxy(proxy_type=proxy_type, + addr=q.socks_proxy_host, + port=q.socks_proxy_port) + socket.socket = socks.socksocket + else: + if six.PY2: + socket.socket = socket._socketobject + elif six.PY3: + import _socket + socket.socket = _socket.socket + + email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type + + if email_box_type == 'pop3': + if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: + if not q.email_box_port: + q.email_box_port = 995 + server = poplib.POP3_SSL(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + else: + if not q.email_box_port: + q.email_box_port = 110 + server = poplib.POP3(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + + logger.info("Attempting POP3 server login") + + server.getwelcome() + server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) + server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) + + messagesInfo = server.list()[1] + logger.info("Received %d messages from POP3 server" % len(messagesInfo)) + + for msg in messagesInfo: + msgNum = msg.split(" ")[0] + logger.info("Processing message %s" % msgNum) + + full_message = "\n".join(server.retr(msgNum)[1]) + ticket = ticket_from_message(message=full_message, queue=q, logger=logger) + + if ticket: + server.dele(msgNum) + logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) + else: + logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) + + server.quit() + + elif email_box_type == 'imap': + if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: + if not q.email_box_port: + q.email_box_port = 993 + server = imaplib.IMAP4_SSL(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + else: + if not q.email_box_port: + q.email_box_port = 143 + server = imaplib.IMAP4(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + + logger.info("Attempting IMAP server login") + + server.login(q.email_box_user or + settings.QUEUE_EMAIL_BOX_USER, + q.email_box_pass or + settings.QUEUE_EMAIL_BOX_PASSWORD) + server.select(q.email_box_imap_folder) + + status, data = server.search(None, 'NOT', 'DELETED') + if data: + msgnums = data[0].split() + logger.info("Received %d messages from IMAP server" % len(msgnums)) + for num in msgnums: + logger.info("Processing message %s" % num) + status, data = server.fetch(num, '(RFC822)') + ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) + if ticket: + server.store(num, '+FLAGS', '\\Deleted') +<<<<<<< HEAD + logger.info("Successfully processed message %s, deleted from IMAP server" % num) + else: + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) +======= + logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) + else: + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) +>>>>>>> PY3 support and msgNum error solved in process_queue method + + server.expunge() + server.close() + server.logout() + + elif email_box_type == 'local': + mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' + mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))] + logger.info("Found %d messages in local mailbox directory" % len(mail)) + + logger.info("Found %d messages in local mailbox directory" % len(mail)) + for i, m in enumerate(mail, 1): + logger.info("Processing message %d" % i) + with open(m, 'r') as f: + ticket = ticket_from_message(message=f.read(), queue=q, logger=logger) + if ticket: + logger.info("Successfully processed message %d, ticket/comment created." % i) + try: + unlink(m) # delete message file if ticket was successful + except: + logger.error("Unable to delete message %d." % i) + else: + logger.info("Successfully deleted message %d." % i) + else: + logger.warn("Message %d was not successfully processed, and will be left in local directory" % i) + + +def decodeUnknown(charset, string): + if six.PY2: + if not charset: + try: + return string.decode('utf-8', 'ignore') + except: + return string.decode('iso8859-1', 'ignore') + return unicode(string, charset) + elif six.PY3: + if type(string) is not str: + if not charset: + try: + return str(string, encoding='utf-8', errors='replace') + except: + return str(string, encoding='iso8859-1', errors='replace') + return str(string, encoding=charset, errors='replace') + return string + + +def decode_mail_headers(string): + decoded = email.header.decode_header(string) + if six.PY2: + return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) + elif six.PY3: + return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded]) + + +def ticket_from_message(message, queue, logger): + # 'message' must be an RFC822 formatted message. +<<<<<<< HEAD + message = email.message_from_string(message) +======= + msg = message + if six.PY2: + message = email.message_from_string(msg) + elif six.PY3: + message = email.message_from_bytes(msg) +>>>>>>> PY3 support and msgNum error solved in process_queue method + subject = message.get('subject', _('Created from e-mail')) + subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) + for affix in STRIPPED_SUBJECT_STRINGS: + subject = subject.replace(affix, "") + subject = subject.strip() + + sender = message.get('from', _('Unknown Sender')) + sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) + sender_email = email.utils.parseaddr(sender)[1] + + for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): + if ignore.test(sender_email): + if ignore.keep_in_mailbox: + # By returning 'False' the message will be kept in the mailbox, + # and the 'True' will cause the message to be deleted. + return False + return True + + matchobj = re.match(r".*\[" + queue.slug + "-(?P\d+)\]", subject) + if matchobj: + # This is a reply or forward. + ticket = matchobj.group('id') + logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) + else: + logger.info("No tracking ID matched.") + ticket = None + + body = None + counter = 0 + files = [] + + for part in message.walk(): + if part.get_content_maintype() == 'multipart': + continue + + name = part.get_param("name") + if name: + name = email.utils.collapse_rfc2231_value(name) + + if part.get_content_maintype() == 'text' and name is None: + if part.get_content_subtype() == 'plain': + body = EmailReplyParser.parse_reply( + decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)) + ) + logger.debug("Discovered plain text MIME part") + else: + files.append( + SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html') + ) + logger.debug("Discovered HTML MIME part") + else: + if not name: + ext = mimetypes.guess_extension(part.get_content_type()) + name = "part-%i%s" % (counter, ext) + files.append(SimpleUploadedFile(name, encoding.smart_bytes(part.get_payload()), part.get_content_type())) + logger.debug("Found MIME attachment %s" % name) + + counter += 1 + + if not body: + body = _('No plain-text email body available. Please see attachment "email_html_body.html".') + + if ticket: + try: + t = Ticket.objects.get(id=ticket) + except Ticket.DoesNotExist: + logger.info("Tracking ID %s-%s not associated with existing ticket. Creating new ticket." % (queue.slug, ticket)) + ticket = None + else: + logger.info("Found existing ticket with Tracking ID %s-%s" % (t.queue.slug, t.id)) + if t.status == Ticket.CLOSED_STATUS: + t.status = Ticket.REOPENED_STATUS + t.save() + new = False + + smtp_priority = message.get('priority', '') + smtp_importance = message.get('importance', '') + high_priority_types = {'high', 'important', '1', 'urgent'} + priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 + + if ticket is None: + new = True + t = Ticket.objects.create( + title=subject, + queue=queue, + submitter_email=sender_email, + created=timezone.now(), + description=body, + priority=priority, + ) + logger.debug("Created new ticket %s-%s" % (t.queue.slug, t.id)) + + f = FollowUp( + ticket=t, + title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), + date=timezone.now(), + public=True, + comment=body, + ) + + if t.status == Ticket.REOPENED_STATUS: + f.new_status = Ticket.REOPENED_STATUS + f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) + + f.save() + logger.debug("Created new FollowUp for Ticket") + + if six.PY2: + logger.info(("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace')) + elif six.PY3: + logger.info("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)) + + attached = process_attachments(f, files) + for att_file in attached: + logger.info("Attachment '%s' successfully added to ticket from email." % att_file[0]) + + context = safe_template_context(t) + + if new: + if sender_email: + send_templated_mail( + 'newticket_submitter', + context, + recipients=sender_email, + sender=queue.from_address, + fail_silently=True, + ) + if queue.new_ticket_cc: + send_templated_mail( + 'newticket_cc', + context, + recipients=queue.new_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc: + send_templated_mail( + 'newticket_cc', + context, + recipients=queue.updated_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + else: + context.update(comment=f.comment) + if t.assigned_to: + send_templated_mail( + 'updated_owner', + context, + recipients=t.assigned_to.email, + sender=queue.from_address, + fail_silently=True, + ) + if queue.updated_ticket_cc: + send_templated_mail( + 'updated_cc', + context, + recipients=queue.updated_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + + return t + + +if __name__ == '__main__': + process_email() From 6482c6338d02005ad4e4886edd4d9963bc1f8086 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Mon, 12 Dec 2016 10:32:01 +0100 Subject: [PATCH 08/45] merged with upstream --- .../management/commands/get_email.py.orig | 448 ------------------ 1 file changed, 448 deletions(-) delete mode 100644 helpdesk/management/commands/get_email.py.orig diff --git a/helpdesk/management/commands/get_email.py.orig b/helpdesk/management/commands/get_email.py.orig deleted file mode 100644 index b662760f..00000000 --- a/helpdesk/management/commands/get_email.py.orig +++ /dev/null @@ -1,448 +0,0 @@ -#!/usr/bin/python -""" -Jutda Helpdesk - A Django powered ticket tracker for small enterprise. - -(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. - -scripts/get_email.py - Designed to be run from cron, this script checks the - POP and IMAP boxes, or a local mailbox directory, - defined for the queues within a - helpdesk, creating tickets from the new messages (or - adding to existing tickets if needed) -""" - -from datetime import timedelta -import email -import imaplib -import mimetypes -from os import listdir, unlink -from os.path import isfile, join -import poplib -import re -import socket -from time import ctime - -from email_reply_parser import EmailReplyParser - -from django.core.files.base import ContentFile -from django.core.files.uploadedfile import SimpleUploadedFile -from django.core.management.base import BaseCommand -from django.db.models import Q -from django.utils.translation import ugettext as _ -from django.utils import encoding, six, timezone - -from helpdesk import settings -from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments -from helpdesk.models import Queue, Ticket, FollowUp, IgnoreEmail - -import logging - - -STRIPPED_SUBJECT_STRINGS = [ - "Re: ", - "Fw: ", - "RE: ", - "FW: ", - "Automatic reply: ", -] - - -class Command(BaseCommand): - - def __init__(self): - BaseCommand.__init__(self) - - help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ - 'from a local mailbox directory as required, feeding them into the helpdesk.' - - def add_arguments(self, parser): - parser.add_argument( - '--quiet', - action='store_true', - dest='quiet', - default=False, - help='Hide details about each queue/message as they are processed', - ) - - def handle(self, *args, **options): - quiet = options.get('quiet', False) - process_email(quiet=quiet) - - -def process_email(quiet=False): - for q in Queue.objects.filter( - email_box_type__isnull=False, - allow_email_submission=True): - - logger = logging.getLogger('django.helpdesk.queue.' + q.slug) - if not q.logging_type or q.logging_type == 'none': - logging.disable(logging.CRITICAL) # disable all messages - elif q.logging_type == 'info': - logger.setLevel(logging.INFO) - elif q.logging_type == 'warn': - logger.setLevel(logging.WARN) - elif q.logging_type == 'error': - logger.setLevel(logging.ERROR) - elif q.logging_type == 'crit': - logger.setLevel(logging.CRITICAL) - elif q.logging_type == 'debug': - logger.setLevel(logging.DEBUG) - if quiet: - logger.propagate = False # do not propagate to root logger that would log to console - logdir = q.logging_dir or '/var/log/helpdesk/' - handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log')) - logger.addHandler(handler) - - if not q.email_box_last_check: - q.email_box_last_check = timezone.now() - timedelta(minutes=30) - - queue_time_delta = timedelta(minutes=q.email_box_interval or 0) - - if (q.email_box_last_check + queue_time_delta) < timezone.now(): - process_queue(q, logger=logger) - q.email_box_last_check = timezone.now() - q.save() - - -def process_queue(q, logger): - logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) - - if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: - try: - import socks - except ImportError: - no_socks_msg = "Queue has been configured with proxy settings, " \ - "but no socks library was installed. Try to " \ - "install PySocks via PyPI." - logger.error(no_socks_msg) - raise ImportError(no_socks_msg) - - proxy_type = { - 'socks4': socks.SOCKS4, - 'socks5': socks.SOCKS5, - }.get(q.socks_proxy_type) - - socks.set_default_proxy(proxy_type=proxy_type, - addr=q.socks_proxy_host, - port=q.socks_proxy_port) - socket.socket = socks.socksocket - else: - if six.PY2: - socket.socket = socket._socketobject - elif six.PY3: - import _socket - socket.socket = _socket.socket - - email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type - - if email_box_type == 'pop3': - if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: - if not q.email_box_port: - q.email_box_port = 995 - server = poplib.POP3_SSL(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - else: - if not q.email_box_port: - q.email_box_port = 110 - server = poplib.POP3(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - - logger.info("Attempting POP3 server login") - - server.getwelcome() - server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) - server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) - - messagesInfo = server.list()[1] - logger.info("Received %d messages from POP3 server" % len(messagesInfo)) - - for msg in messagesInfo: - msgNum = msg.split(" ")[0] - logger.info("Processing message %s" % msgNum) - - full_message = "\n".join(server.retr(msgNum)[1]) - ticket = ticket_from_message(message=full_message, queue=q, logger=logger) - - if ticket: - server.dele(msgNum) - logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) - else: - logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) - - server.quit() - - elif email_box_type == 'imap': - if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: - if not q.email_box_port: - q.email_box_port = 993 - server = imaplib.IMAP4_SSL(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - else: - if not q.email_box_port: - q.email_box_port = 143 - server = imaplib.IMAP4(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - - logger.info("Attempting IMAP server login") - - server.login(q.email_box_user or - settings.QUEUE_EMAIL_BOX_USER, - q.email_box_pass or - settings.QUEUE_EMAIL_BOX_PASSWORD) - server.select(q.email_box_imap_folder) - - status, data = server.search(None, 'NOT', 'DELETED') - if data: - msgnums = data[0].split() - logger.info("Received %d messages from IMAP server" % len(msgnums)) - for num in msgnums: - logger.info("Processing message %s" % num) - status, data = server.fetch(num, '(RFC822)') - ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) - if ticket: - server.store(num, '+FLAGS', '\\Deleted') -<<<<<<< HEAD - logger.info("Successfully processed message %s, deleted from IMAP server" % num) - else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) -======= - logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) - else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) ->>>>>>> PY3 support and msgNum error solved in process_queue method - - server.expunge() - server.close() - server.logout() - - elif email_box_type == 'local': - mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' - mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))] - logger.info("Found %d messages in local mailbox directory" % len(mail)) - - logger.info("Found %d messages in local mailbox directory" % len(mail)) - for i, m in enumerate(mail, 1): - logger.info("Processing message %d" % i) - with open(m, 'r') as f: - ticket = ticket_from_message(message=f.read(), queue=q, logger=logger) - if ticket: - logger.info("Successfully processed message %d, ticket/comment created." % i) - try: - unlink(m) # delete message file if ticket was successful - except: - logger.error("Unable to delete message %d." % i) - else: - logger.info("Successfully deleted message %d." % i) - else: - logger.warn("Message %d was not successfully processed, and will be left in local directory" % i) - - -def decodeUnknown(charset, string): - if six.PY2: - if not charset: - try: - return string.decode('utf-8', 'ignore') - except: - return string.decode('iso8859-1', 'ignore') - return unicode(string, charset) - elif six.PY3: - if type(string) is not str: - if not charset: - try: - return str(string, encoding='utf-8', errors='replace') - except: - return str(string, encoding='iso8859-1', errors='replace') - return str(string, encoding=charset, errors='replace') - return string - - -def decode_mail_headers(string): - decoded = email.header.decode_header(string) - if six.PY2: - return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) - elif six.PY3: - return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded]) - - -def ticket_from_message(message, queue, logger): - # 'message' must be an RFC822 formatted message. -<<<<<<< HEAD - message = email.message_from_string(message) -======= - msg = message - if six.PY2: - message = email.message_from_string(msg) - elif six.PY3: - message = email.message_from_bytes(msg) ->>>>>>> PY3 support and msgNum error solved in process_queue method - subject = message.get('subject', _('Created from e-mail')) - subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) - for affix in STRIPPED_SUBJECT_STRINGS: - subject = subject.replace(affix, "") - subject = subject.strip() - - sender = message.get('from', _('Unknown Sender')) - sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) - sender_email = email.utils.parseaddr(sender)[1] - - for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): - if ignore.test(sender_email): - if ignore.keep_in_mailbox: - # By returning 'False' the message will be kept in the mailbox, - # and the 'True' will cause the message to be deleted. - return False - return True - - matchobj = re.match(r".*\[" + queue.slug + "-(?P\d+)\]", subject) - if matchobj: - # This is a reply or forward. - ticket = matchobj.group('id') - logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) - else: - logger.info("No tracking ID matched.") - ticket = None - - body = None - counter = 0 - files = [] - - for part in message.walk(): - if part.get_content_maintype() == 'multipart': - continue - - name = part.get_param("name") - if name: - name = email.utils.collapse_rfc2231_value(name) - - if part.get_content_maintype() == 'text' and name is None: - if part.get_content_subtype() == 'plain': - body = EmailReplyParser.parse_reply( - decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)) - ) - logger.debug("Discovered plain text MIME part") - else: - files.append( - SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html') - ) - logger.debug("Discovered HTML MIME part") - else: - if not name: - ext = mimetypes.guess_extension(part.get_content_type()) - name = "part-%i%s" % (counter, ext) - files.append(SimpleUploadedFile(name, encoding.smart_bytes(part.get_payload()), part.get_content_type())) - logger.debug("Found MIME attachment %s" % name) - - counter += 1 - - if not body: - body = _('No plain-text email body available. Please see attachment "email_html_body.html".') - - if ticket: - try: - t = Ticket.objects.get(id=ticket) - except Ticket.DoesNotExist: - logger.info("Tracking ID %s-%s not associated with existing ticket. Creating new ticket." % (queue.slug, ticket)) - ticket = None - else: - logger.info("Found existing ticket with Tracking ID %s-%s" % (t.queue.slug, t.id)) - if t.status == Ticket.CLOSED_STATUS: - t.status = Ticket.REOPENED_STATUS - t.save() - new = False - - smtp_priority = message.get('priority', '') - smtp_importance = message.get('importance', '') - high_priority_types = {'high', 'important', '1', 'urgent'} - priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 - - if ticket is None: - new = True - t = Ticket.objects.create( - title=subject, - queue=queue, - submitter_email=sender_email, - created=timezone.now(), - description=body, - priority=priority, - ) - logger.debug("Created new ticket %s-%s" % (t.queue.slug, t.id)) - - f = FollowUp( - ticket=t, - title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), - date=timezone.now(), - public=True, - comment=body, - ) - - if t.status == Ticket.REOPENED_STATUS: - f.new_status = Ticket.REOPENED_STATUS - f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) - - f.save() - logger.debug("Created new FollowUp for Ticket") - - if six.PY2: - logger.info(("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace')) - elif six.PY3: - logger.info("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)) - - attached = process_attachments(f, files) - for att_file in attached: - logger.info("Attachment '%s' successfully added to ticket from email." % att_file[0]) - - context = safe_template_context(t) - - if new: - if sender_email: - send_templated_mail( - 'newticket_submitter', - context, - recipients=sender_email, - sender=queue.from_address, - fail_silently=True, - ) - if queue.new_ticket_cc: - send_templated_mail( - 'newticket_cc', - context, - recipients=queue.new_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc: - send_templated_mail( - 'newticket_cc', - context, - recipients=queue.updated_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - else: - context.update(comment=f.comment) - if t.assigned_to: - send_templated_mail( - 'updated_owner', - context, - recipients=t.assigned_to.email, - sender=queue.from_address, - fail_silently=True, - ) - if queue.updated_ticket_cc: - send_templated_mail( - 'updated_cc', - context, - recipients=queue.updated_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - - return t - - -if __name__ == '__main__': - process_email() From e326fea099f0ae3b7ff496687e0fe53c31118daa Mon Sep 17 00:00:00 2001 From: Pawel M Date: Mon, 12 Dec 2016 11:15:45 +0100 Subject: [PATCH 09/45] merged with upstream --- helpdesk/management/commands/get_email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 6babf404..0bf4c0a3 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -205,9 +205,9 @@ def process_queue(q, logger): ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) if ticket: server.store(num, '+FLAGS', '\\Deleted') - logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) + logger.info("Successfully processed message %s, deleted from IMAP server" % num) else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num) server.expunge() server.close() From 8dbd54ac168202db5cfed9400d416a66300470a9 Mon Sep 17 00:00:00 2001 From: Jonathan Barratt Date: Mon, 12 Dec 2016 22:13:57 +0700 Subject: [PATCH 10/45] stop corrupting binary attachments when delivering them by email 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. --- helpdesk/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 73d309c0..8619fa26 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -21,6 +21,7 @@ except ImportError: 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 @@ -121,7 +122,11 @@ def send_templated_mail(template_name, if files: for filename, filefield in files: - msg.attach(filename, open(filefield.path).read()) + 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) From e7c4131ed7e37bf6ca6499bfa9c6d42ed98855f1 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Tue, 13 Dec 2016 22:39:28 +0100 Subject: [PATCH 11/45] Attachement should be readed in binary mode ... and attached as payload (regarding docs) --- helpdesk/lib.py | 10 +- helpdesk/management/commands/get_email.py | 3 +- .../management/commands/get_email.py.orig | 434 ++++++++++++++++++ 3 files changed, 439 insertions(+), 8 deletions(-) create mode 100644 helpdesk/management/commands/get_email.py.orig diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 8619fa26..a87821ce 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -21,7 +21,6 @@ except ImportError: 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 @@ -121,12 +120,9 @@ def send_templated_mail(template_name, 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()) + for file_name, file_field in files: + with open(file_field.path, 'rb') as attached_file: + msg.attach(file_name, attached_file.read(), getattr(attached_file, 'content_type', None)) return msg.send(fail_silently) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 0bf4c0a3..9360dc46 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -319,7 +319,8 @@ def ticket_from_message(message, queue, logger): if not name: ext = mimetypes.guess_extension(part.get_content_type()) name = "part-%i%s" % (counter, ext) - files.append(SimpleUploadedFile(name, encoding.smart_bytes(part.get_payload()), part.get_content_type())) + print(name, part.get_content_type()) + files.append(SimpleUploadedFile(name, part.get_payload(decode=True), part.get_content_type())) logger.debug("Found MIME attachment %s" % name) counter += 1 diff --git a/helpdesk/management/commands/get_email.py.orig b/helpdesk/management/commands/get_email.py.orig new file mode 100644 index 00000000..6babf404 --- /dev/null +++ b/helpdesk/management/commands/get_email.py.orig @@ -0,0 +1,434 @@ +#!/usr/bin/python +""" +Jutda Helpdesk - A Django powered ticket tracker for small enterprise. + +(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. + +scripts/get_email.py - Designed to be run from cron, this script checks the + POP and IMAP boxes, or a local mailbox directory, + defined for the queues within a + helpdesk, creating tickets from the new messages (or + adding to existing tickets if needed) +""" + +from datetime import timedelta +import email +import imaplib +import mimetypes +from os import listdir, unlink +from os.path import isfile, join +import poplib +import re +import socket +from time import ctime + +from email_reply_parser import EmailReplyParser + +from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management.base import BaseCommand +from django.db.models import Q +from django.utils.translation import ugettext as _ +from django.utils import encoding, six, timezone + +from helpdesk import settings +from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments +from helpdesk.models import Queue, Ticket, FollowUp, IgnoreEmail + +import logging + + +STRIPPED_SUBJECT_STRINGS = [ + "Re: ", + "Fw: ", + "RE: ", + "FW: ", + "Automatic reply: ", +] + + +class Command(BaseCommand): + + def __init__(self): + BaseCommand.__init__(self) + + help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ + 'from a local mailbox directory as required, feeding them into the helpdesk.' + + def add_arguments(self, parser): + parser.add_argument( + '--quiet', + action='store_true', + dest='quiet', + default=False, + help='Hide details about each queue/message as they are processed', + ) + + def handle(self, *args, **options): + quiet = options.get('quiet', False) + process_email(quiet=quiet) + + +def process_email(quiet=False): + for q in Queue.objects.filter( + email_box_type__isnull=False, + allow_email_submission=True): + + logger = logging.getLogger('django.helpdesk.queue.' + q.slug) + if not q.logging_type or q.logging_type == 'none': + logging.disable(logging.CRITICAL) # disable all messages + elif q.logging_type == 'info': + logger.setLevel(logging.INFO) + elif q.logging_type == 'warn': + logger.setLevel(logging.WARN) + elif q.logging_type == 'error': + logger.setLevel(logging.ERROR) + elif q.logging_type == 'crit': + logger.setLevel(logging.CRITICAL) + elif q.logging_type == 'debug': + logger.setLevel(logging.DEBUG) + if quiet: + logger.propagate = False # do not propagate to root logger that would log to console + logdir = q.logging_dir or '/var/log/helpdesk/' + handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log')) + logger.addHandler(handler) + + if not q.email_box_last_check: + q.email_box_last_check = timezone.now() - timedelta(minutes=30) + + queue_time_delta = timedelta(minutes=q.email_box_interval or 0) + + if (q.email_box_last_check + queue_time_delta) < timezone.now(): + process_queue(q, logger=logger) + q.email_box_last_check = timezone.now() + q.save() + + +def process_queue(q, logger): + logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) + + if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: + try: + import socks + except ImportError: + no_socks_msg = "Queue has been configured with proxy settings, " \ + "but no socks library was installed. Try to " \ + "install PySocks via PyPI." + logger.error(no_socks_msg) + raise ImportError(no_socks_msg) + + proxy_type = { + 'socks4': socks.SOCKS4, + 'socks5': socks.SOCKS5, + }.get(q.socks_proxy_type) + + socks.set_default_proxy(proxy_type=proxy_type, + addr=q.socks_proxy_host, + port=q.socks_proxy_port) + socket.socket = socks.socksocket + else: + if six.PY2: + socket.socket = socket._socketobject + elif six.PY3: + import _socket + socket.socket = _socket.socket + + email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type + + if email_box_type == 'pop3': + if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: + if not q.email_box_port: + q.email_box_port = 995 + server = poplib.POP3_SSL(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + else: + if not q.email_box_port: + q.email_box_port = 110 + server = poplib.POP3(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + + logger.info("Attempting POP3 server login") + + server.getwelcome() + server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) + server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) + + messagesInfo = server.list()[1] + logger.info("Received %d messages from POP3 server" % len(messagesInfo)) + + for msg in messagesInfo: + msgNum = msg.split(" ")[0] + logger.info("Processing message %s" % msgNum) + + full_message = "\n".join(server.retr(msgNum)[1]) + ticket = ticket_from_message(message=full_message, queue=q, logger=logger) + + if ticket: + server.dele(msgNum) + logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) + else: + logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) + + server.quit() + + elif email_box_type == 'imap': + if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: + if not q.email_box_port: + q.email_box_port = 993 + server = imaplib.IMAP4_SSL(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + else: + if not q.email_box_port: + q.email_box_port = 143 + server = imaplib.IMAP4(q.email_box_host or + settings.QUEUE_EMAIL_BOX_HOST, + int(q.email_box_port)) + + logger.info("Attempting IMAP server login") + + server.login(q.email_box_user or + settings.QUEUE_EMAIL_BOX_USER, + q.email_box_pass or + settings.QUEUE_EMAIL_BOX_PASSWORD) + server.select(q.email_box_imap_folder) + + status, data = server.search(None, 'NOT', 'DELETED') + if data: + msgnums = data[0].split() + logger.info("Received %d messages from IMAP server" % len(msgnums)) + for num in msgnums: + logger.info("Processing message %s" % num) + status, data = server.fetch(num, '(RFC822)') + ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) + if ticket: + server.store(num, '+FLAGS', '\\Deleted') + logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) + else: + logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) + + server.expunge() + server.close() + server.logout() + + elif email_box_type == 'local': + mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' + mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))] + logger.info("Found %d messages in local mailbox directory" % len(mail)) + + logger.info("Found %d messages in local mailbox directory" % len(mail)) + for i, m in enumerate(mail, 1): + logger.info("Processing message %d" % i) + with open(m, 'r') as f: + ticket = ticket_from_message(message=f.read(), queue=q, logger=logger) + if ticket: + logger.info("Successfully processed message %d, ticket/comment created." % i) + try: + unlink(m) # delete message file if ticket was successful + except: + logger.error("Unable to delete message %d." % i) + else: + logger.info("Successfully deleted message %d." % i) + else: + logger.warn("Message %d was not successfully processed, and will be left in local directory" % i) + + +def decodeUnknown(charset, string): + if six.PY2: + if not charset: + try: + return string.decode('utf-8', 'ignore') + except: + return string.decode('iso8859-1', 'ignore') + return unicode(string, charset) + elif six.PY3: + if type(string) is not str: + if not charset: + try: + return str(string, encoding='utf-8', errors='replace') + except: + return str(string, encoding='iso8859-1', errors='replace') + return str(string, encoding=charset, errors='replace') + return string + + +def decode_mail_headers(string): + decoded = email.header.decode_header(string) + if six.PY2: + return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) + elif six.PY3: + return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded]) + + +def ticket_from_message(message, queue, logger): + # 'message' must be an RFC822 formatted message. + message = email.message_from_string(message) + subject = message.get('subject', _('Created from e-mail')) + subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) + for affix in STRIPPED_SUBJECT_STRINGS: + subject = subject.replace(affix, "") + subject = subject.strip() + + sender = message.get('from', _('Unknown Sender')) + sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) + sender_email = email.utils.parseaddr(sender)[1] + + for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): + if ignore.test(sender_email): + if ignore.keep_in_mailbox: + # By returning 'False' the message will be kept in the mailbox, + # and the 'True' will cause the message to be deleted. + return False + return True + + matchobj = re.match(r".*\[" + queue.slug + "-(?P\d+)\]", subject) + if matchobj: + # This is a reply or forward. + ticket = matchobj.group('id') + logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) + else: + logger.info("No tracking ID matched.") + ticket = None + + body = None + counter = 0 + files = [] + + for part in message.walk(): + if part.get_content_maintype() == 'multipart': + continue + + name = part.get_param("name") + if name: + name = email.utils.collapse_rfc2231_value(name) + + if part.get_content_maintype() == 'text' and name is None: + if part.get_content_subtype() == 'plain': + body = EmailReplyParser.parse_reply( + decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)) + ) + logger.debug("Discovered plain text MIME part") + else: + files.append( + SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html') + ) + logger.debug("Discovered HTML MIME part") + else: + if not name: + ext = mimetypes.guess_extension(part.get_content_type()) + name = "part-%i%s" % (counter, ext) + files.append(SimpleUploadedFile(name, encoding.smart_bytes(part.get_payload()), part.get_content_type())) + logger.debug("Found MIME attachment %s" % name) + + counter += 1 + + if not body: + body = _('No plain-text email body available. Please see attachment "email_html_body.html".') + + if ticket: + try: + t = Ticket.objects.get(id=ticket) + except Ticket.DoesNotExist: + logger.info("Tracking ID %s-%s not associated with existing ticket. Creating new ticket." % (queue.slug, ticket)) + ticket = None + else: + logger.info("Found existing ticket with Tracking ID %s-%s" % (t.queue.slug, t.id)) + if t.status == Ticket.CLOSED_STATUS: + t.status = Ticket.REOPENED_STATUS + t.save() + new = False + + smtp_priority = message.get('priority', '') + smtp_importance = message.get('importance', '') + high_priority_types = {'high', 'important', '1', 'urgent'} + priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 + + if ticket is None: + new = True + t = Ticket.objects.create( + title=subject, + queue=queue, + submitter_email=sender_email, + created=timezone.now(), + description=body, + priority=priority, + ) + logger.debug("Created new ticket %s-%s" % (t.queue.slug, t.id)) + + f = FollowUp( + ticket=t, + title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), + date=timezone.now(), + public=True, + comment=body, + ) + + if t.status == Ticket.REOPENED_STATUS: + f.new_status = Ticket.REOPENED_STATUS + f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) + + f.save() + logger.debug("Created new FollowUp for Ticket") + + if six.PY2: + logger.info(("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace')) + elif six.PY3: + logger.info("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)) + + attached = process_attachments(f, files) + for att_file in attached: + logger.info("Attachment '%s' successfully added to ticket from email." % att_file[0]) + + context = safe_template_context(t) + + if new: + if sender_email: + send_templated_mail( + 'newticket_submitter', + context, + recipients=sender_email, + sender=queue.from_address, + fail_silently=True, + ) + if queue.new_ticket_cc: + send_templated_mail( + 'newticket_cc', + context, + recipients=queue.new_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc: + send_templated_mail( + 'newticket_cc', + context, + recipients=queue.updated_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + else: + context.update(comment=f.comment) + if t.assigned_to: + send_templated_mail( + 'updated_owner', + context, + recipients=t.assigned_to.email, + sender=queue.from_address, + fail_silently=True, + ) + if queue.updated_ticket_cc: + send_templated_mail( + 'updated_cc', + context, + recipients=queue.updated_ticket_cc, + sender=queue.from_address, + fail_silently=True, + ) + + return t + + +if __name__ == '__main__': + process_email() From 9939f62ebda35b6c14a3a7583c66c56a0594d24b Mon Sep 17 00:00:00 2001 From: Pawel M Date: Tue, 13 Dec 2016 23:28:16 +0100 Subject: [PATCH 12/45] Attachement should be readed in binary mode ... and attached as payload (regarding docs) --- helpdesk/lib.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index a87821ce..ac309df5 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -118,11 +118,14 @@ def send_templated_mail(template_name, sender or settings.DEFAULT_FROM_EMAIL, recipients, bcc=bcc) msg.attach_alternative(html_part, "text/html") - + if files: for file_name, file_field in files: - with open(file_field.path, 'rb') as attached_file: - msg.attach(file_name, attached_file.read(), getattr(attached_file, 'content_type', None)) + part_attachment = MIMEBase('application', "octet-stream") + part_attachment.set_payload(open(file_field.path, 'rb').read()) + encoders.encode_base64(part_attachment) + part_attachment.add_header('Content-Disposition', 'attachment; filename="{}"'.format(file_name)) + msg.attach(part_attachment) return msg.send(fail_silently) From f5b68772cfd5262a506a9d8456133dc2101e785b Mon Sep 17 00:00:00 2001 From: Pawel M Date: Tue, 13 Dec 2016 23:31:26 +0100 Subject: [PATCH 13/45] Downloaded attachment was not decoded and additionally encoded by default django method (its size after download is bigger than originally attached file). This is the reason why it is illegible --- helpdesk/management/commands/get_email.py | 3 +- .../management/commands/get_email.py.orig | 434 ------------------ 2 files changed, 1 insertion(+), 436 deletions(-) delete mode 100644 helpdesk/management/commands/get_email.py.orig diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 9360dc46..8aecb5fa 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -319,8 +319,7 @@ def ticket_from_message(message, queue, logger): if not name: ext = mimetypes.guess_extension(part.get_content_type()) name = "part-%i%s" % (counter, ext) - print(name, part.get_content_type()) - files.append(SimpleUploadedFile(name, part.get_payload(decode=True), part.get_content_type())) + files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) logger.debug("Found MIME attachment %s" % name) counter += 1 diff --git a/helpdesk/management/commands/get_email.py.orig b/helpdesk/management/commands/get_email.py.orig deleted file mode 100644 index 6babf404..00000000 --- a/helpdesk/management/commands/get_email.py.orig +++ /dev/null @@ -1,434 +0,0 @@ -#!/usr/bin/python -""" -Jutda Helpdesk - A Django powered ticket tracker for small enterprise. - -(c) Copyright 2008 Jutda. All Rights Reserved. See LICENSE for details. - -scripts/get_email.py - Designed to be run from cron, this script checks the - POP and IMAP boxes, or a local mailbox directory, - defined for the queues within a - helpdesk, creating tickets from the new messages (or - adding to existing tickets if needed) -""" - -from datetime import timedelta -import email -import imaplib -import mimetypes -from os import listdir, unlink -from os.path import isfile, join -import poplib -import re -import socket -from time import ctime - -from email_reply_parser import EmailReplyParser - -from django.core.files.base import ContentFile -from django.core.files.uploadedfile import SimpleUploadedFile -from django.core.management.base import BaseCommand -from django.db.models import Q -from django.utils.translation import ugettext as _ -from django.utils import encoding, six, timezone - -from helpdesk import settings -from helpdesk.lib import send_templated_mail, safe_template_context, process_attachments -from helpdesk.models import Queue, Ticket, FollowUp, IgnoreEmail - -import logging - - -STRIPPED_SUBJECT_STRINGS = [ - "Re: ", - "Fw: ", - "RE: ", - "FW: ", - "Automatic reply: ", -] - - -class Command(BaseCommand): - - def __init__(self): - BaseCommand.__init__(self) - - help = 'Process django-helpdesk queues and process e-mails via POP3/IMAP or ' \ - 'from a local mailbox directory as required, feeding them into the helpdesk.' - - def add_arguments(self, parser): - parser.add_argument( - '--quiet', - action='store_true', - dest='quiet', - default=False, - help='Hide details about each queue/message as they are processed', - ) - - def handle(self, *args, **options): - quiet = options.get('quiet', False) - process_email(quiet=quiet) - - -def process_email(quiet=False): - for q in Queue.objects.filter( - email_box_type__isnull=False, - allow_email_submission=True): - - logger = logging.getLogger('django.helpdesk.queue.' + q.slug) - if not q.logging_type or q.logging_type == 'none': - logging.disable(logging.CRITICAL) # disable all messages - elif q.logging_type == 'info': - logger.setLevel(logging.INFO) - elif q.logging_type == 'warn': - logger.setLevel(logging.WARN) - elif q.logging_type == 'error': - logger.setLevel(logging.ERROR) - elif q.logging_type == 'crit': - logger.setLevel(logging.CRITICAL) - elif q.logging_type == 'debug': - logger.setLevel(logging.DEBUG) - if quiet: - logger.propagate = False # do not propagate to root logger that would log to console - logdir = q.logging_dir or '/var/log/helpdesk/' - handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log')) - logger.addHandler(handler) - - if not q.email_box_last_check: - q.email_box_last_check = timezone.now() - timedelta(minutes=30) - - queue_time_delta = timedelta(minutes=q.email_box_interval or 0) - - if (q.email_box_last_check + queue_time_delta) < timezone.now(): - process_queue(q, logger=logger) - q.email_box_last_check = timezone.now() - q.save() - - -def process_queue(q, logger): - logger.info("***** %s: Begin processing mail for django-helpdesk" % ctime()) - - if q.socks_proxy_type and q.socks_proxy_host and q.socks_proxy_port: - try: - import socks - except ImportError: - no_socks_msg = "Queue has been configured with proxy settings, " \ - "but no socks library was installed. Try to " \ - "install PySocks via PyPI." - logger.error(no_socks_msg) - raise ImportError(no_socks_msg) - - proxy_type = { - 'socks4': socks.SOCKS4, - 'socks5': socks.SOCKS5, - }.get(q.socks_proxy_type) - - socks.set_default_proxy(proxy_type=proxy_type, - addr=q.socks_proxy_host, - port=q.socks_proxy_port) - socket.socket = socks.socksocket - else: - if six.PY2: - socket.socket = socket._socketobject - elif six.PY3: - import _socket - socket.socket = _socket.socket - - email_box_type = settings.QUEUE_EMAIL_BOX_TYPE or q.email_box_type - - if email_box_type == 'pop3': - if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: - if not q.email_box_port: - q.email_box_port = 995 - server = poplib.POP3_SSL(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - else: - if not q.email_box_port: - q.email_box_port = 110 - server = poplib.POP3(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - - logger.info("Attempting POP3 server login") - - server.getwelcome() - server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER) - server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD) - - messagesInfo = server.list()[1] - logger.info("Received %d messages from POP3 server" % len(messagesInfo)) - - for msg in messagesInfo: - msgNum = msg.split(" ")[0] - logger.info("Processing message %s" % msgNum) - - full_message = "\n".join(server.retr(msgNum)[1]) - ticket = ticket_from_message(message=full_message, queue=q, logger=logger) - - if ticket: - server.dele(msgNum) - logger.info("Successfully processed message %s, deleted from POP3 server" % msgNum) - else: - logger.warn("Message %s was not successfully processed, and will be left on POP3 server" % msgNum) - - server.quit() - - elif email_box_type == 'imap': - if q.email_box_ssl or settings.QUEUE_EMAIL_BOX_SSL: - if not q.email_box_port: - q.email_box_port = 993 - server = imaplib.IMAP4_SSL(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - else: - if not q.email_box_port: - q.email_box_port = 143 - server = imaplib.IMAP4(q.email_box_host or - settings.QUEUE_EMAIL_BOX_HOST, - int(q.email_box_port)) - - logger.info("Attempting IMAP server login") - - server.login(q.email_box_user or - settings.QUEUE_EMAIL_BOX_USER, - q.email_box_pass or - settings.QUEUE_EMAIL_BOX_PASSWORD) - server.select(q.email_box_imap_folder) - - status, data = server.search(None, 'NOT', 'DELETED') - if data: - msgnums = data[0].split() - logger.info("Received %d messages from IMAP server" % len(msgnums)) - for num in msgnums: - logger.info("Processing message %s" % num) - status, data = server.fetch(num, '(RFC822)') - ticket = ticket_from_message(message=encoding.smart_text(data[0][1]), queue=q, logger=logger) - if ticket: - server.store(num, '+FLAGS', '\\Deleted') - logger.info("Successfully processed message %s, deleted from IMAP server" % str(num)) - else: - logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % str(num)) - - server.expunge() - server.close() - server.logout() - - elif email_box_type == 'local': - mail_dir = q.email_box_local_dir or '/var/lib/mail/helpdesk/' - mail = [join(mail_dir, f) for f in listdir(mail_dir) if isfile(join(mail_dir, f))] - logger.info("Found %d messages in local mailbox directory" % len(mail)) - - logger.info("Found %d messages in local mailbox directory" % len(mail)) - for i, m in enumerate(mail, 1): - logger.info("Processing message %d" % i) - with open(m, 'r') as f: - ticket = ticket_from_message(message=f.read(), queue=q, logger=logger) - if ticket: - logger.info("Successfully processed message %d, ticket/comment created." % i) - try: - unlink(m) # delete message file if ticket was successful - except: - logger.error("Unable to delete message %d." % i) - else: - logger.info("Successfully deleted message %d." % i) - else: - logger.warn("Message %d was not successfully processed, and will be left in local directory" % i) - - -def decodeUnknown(charset, string): - if six.PY2: - if not charset: - try: - return string.decode('utf-8', 'ignore') - except: - return string.decode('iso8859-1', 'ignore') - return unicode(string, charset) - elif six.PY3: - if type(string) is not str: - if not charset: - try: - return str(string, encoding='utf-8', errors='replace') - except: - return str(string, encoding='iso8859-1', errors='replace') - return str(string, encoding=charset, errors='replace') - return string - - -def decode_mail_headers(string): - decoded = email.header.decode_header(string) - if six.PY2: - return u' '.join([unicode(msg, charset or 'utf-8') for msg, charset in decoded]) - elif six.PY3: - return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded]) - - -def ticket_from_message(message, queue, logger): - # 'message' must be an RFC822 formatted message. - message = email.message_from_string(message) - subject = message.get('subject', _('Created from e-mail')) - subject = decode_mail_headers(decodeUnknown(message.get_charset(), subject)) - for affix in STRIPPED_SUBJECT_STRINGS: - subject = subject.replace(affix, "") - subject = subject.strip() - - sender = message.get('from', _('Unknown Sender')) - sender = decode_mail_headers(decodeUnknown(message.get_charset(), sender)) - sender_email = email.utils.parseaddr(sender)[1] - - for ignore in IgnoreEmail.objects.filter(Q(queues=queue) | Q(queues__isnull=True)): - if ignore.test(sender_email): - if ignore.keep_in_mailbox: - # By returning 'False' the message will be kept in the mailbox, - # and the 'True' will cause the message to be deleted. - return False - return True - - matchobj = re.match(r".*\[" + queue.slug + "-(?P\d+)\]", subject) - if matchobj: - # This is a reply or forward. - ticket = matchobj.group('id') - logger.info("Matched tracking ID %s-%s" % (queue.slug, ticket)) - else: - logger.info("No tracking ID matched.") - ticket = None - - body = None - counter = 0 - files = [] - - for part in message.walk(): - if part.get_content_maintype() == 'multipart': - continue - - name = part.get_param("name") - if name: - name = email.utils.collapse_rfc2231_value(name) - - if part.get_content_maintype() == 'text' and name is None: - if part.get_content_subtype() == 'plain': - body = EmailReplyParser.parse_reply( - decodeUnknown(part.get_content_charset(), part.get_payload(decode=True)) - ) - logger.debug("Discovered plain text MIME part") - else: - files.append( - SimpleUploadedFile(_("email_html_body.html"), encoding.smart_bytes(part.get_payload()), 'text/html') - ) - logger.debug("Discovered HTML MIME part") - else: - if not name: - ext = mimetypes.guess_extension(part.get_content_type()) - name = "part-%i%s" % (counter, ext) - files.append(SimpleUploadedFile(name, encoding.smart_bytes(part.get_payload()), part.get_content_type())) - logger.debug("Found MIME attachment %s" % name) - - counter += 1 - - if not body: - body = _('No plain-text email body available. Please see attachment "email_html_body.html".') - - if ticket: - try: - t = Ticket.objects.get(id=ticket) - except Ticket.DoesNotExist: - logger.info("Tracking ID %s-%s not associated with existing ticket. Creating new ticket." % (queue.slug, ticket)) - ticket = None - else: - logger.info("Found existing ticket with Tracking ID %s-%s" % (t.queue.slug, t.id)) - if t.status == Ticket.CLOSED_STATUS: - t.status = Ticket.REOPENED_STATUS - t.save() - new = False - - smtp_priority = message.get('priority', '') - smtp_importance = message.get('importance', '') - high_priority_types = {'high', 'important', '1', 'urgent'} - priority = 2 if high_priority_types & {smtp_priority, smtp_importance} else 3 - - if ticket is None: - new = True - t = Ticket.objects.create( - title=subject, - queue=queue, - submitter_email=sender_email, - created=timezone.now(), - description=body, - priority=priority, - ) - logger.debug("Created new ticket %s-%s" % (t.queue.slug, t.id)) - - f = FollowUp( - ticket=t, - title=_('E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}), - date=timezone.now(), - public=True, - comment=body, - ) - - if t.status == Ticket.REOPENED_STATUS: - f.new_status = Ticket.REOPENED_STATUS - f.title = _('Ticket Re-Opened by E-Mail Received from %(sender_email)s' % {'sender_email': sender_email}) - - f.save() - logger.debug("Created new FollowUp for Ticket") - - if six.PY2: - logger.info(("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)).encode('ascii', 'replace')) - elif six.PY3: - logger.info("[%s-%s] %s" % (t.queue.slug, t.id, t.title,)) - - attached = process_attachments(f, files) - for att_file in attached: - logger.info("Attachment '%s' successfully added to ticket from email." % att_file[0]) - - context = safe_template_context(t) - - if new: - if sender_email: - send_templated_mail( - 'newticket_submitter', - context, - recipients=sender_email, - sender=queue.from_address, - fail_silently=True, - ) - if queue.new_ticket_cc: - send_templated_mail( - 'newticket_cc', - context, - recipients=queue.new_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - if queue.updated_ticket_cc and queue.updated_ticket_cc != queue.new_ticket_cc: - send_templated_mail( - 'newticket_cc', - context, - recipients=queue.updated_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - else: - context.update(comment=f.comment) - if t.assigned_to: - send_templated_mail( - 'updated_owner', - context, - recipients=t.assigned_to.email, - sender=queue.from_address, - fail_silently=True, - ) - if queue.updated_ticket_cc: - send_templated_mail( - 'updated_cc', - context, - recipients=queue.updated_ticket_cc, - sender=queue.from_address, - fail_silently=True, - ) - - return t - - -if __name__ == '__main__': - process_email() From 3a372fbcb4fee48546166115e44fbb8ac0b51824 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Tue, 13 Dec 2016 23:40:36 +0100 Subject: [PATCH 14/45] White space removed --- helpdesk/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index ac309df5..2d406064 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -118,7 +118,7 @@ def send_templated_mail(template_name, sender or settings.DEFAULT_FROM_EMAIL, recipients, bcc=bcc) msg.attach_alternative(html_part, "text/html") - + if files: for file_name, file_field in files: part_attachment = MIMEBase('application', "octet-stream") From df7920cd2e014abc94cd2b080d64682266945521 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Wed, 14 Dec 2016 00:04:38 +0100 Subject: [PATCH 15/45] missing imports added --- helpdesk/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 2d406064..1f45cd63 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -64,6 +64,8 @@ def send_templated_mail(template_name, """ from django.core.mail import EmailMultiAlternatives from django.template import engines + from email.mime.base import MIMEBase + from email import encoders from_string = engines['django'].from_string from helpdesk.models import EmailTemplate From fc0ae58420a1c45e91f0136e5e8752bd399f1a50 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Fri, 16 Dec 2016 10:30:50 +0100 Subject: [PATCH 16/45] lib as reduxionist + read binary mode --- helpdesk/lib.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 1f45cd63..032ce3d7 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -21,6 +21,7 @@ except ImportError: 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 @@ -64,8 +65,6 @@ def send_templated_mail(template_name, """ from django.core.mail import EmailMultiAlternatives from django.template import engines - from email.mime.base import MIMEBase - from email import encoders from_string = engines['django'].from_string from helpdesk.models import EmailTemplate @@ -121,13 +120,12 @@ def send_templated_mail(template_name, recipients, bcc=bcc) msg.attach_alternative(html_part, "text/html") - if files: - for file_name, file_field in files: - part_attachment = MIMEBase('application', "octet-stream") - part_attachment.set_payload(open(file_field.path, 'rb').read()) - encoders.encode_base64(part_attachment) - part_attachment.add_header('Content-Disposition', 'attachment; filename="{}"'.format(file_name)) - msg.attach(part_attachment) + for filename, filefield in files: + if six.PY3: + msg.attach_file(filefield.path) + else: + with open(filefield.path, 'rb') as attachedfile: + msg.attach(filename, attachedfile.read()) return msg.send(fail_silently) From 97b3444a922b5fa52b2fa733971a9e0e123d2db0 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Fri, 16 Dec 2016 10:36:00 +0100 Subject: [PATCH 17/45] if files condition restored --- helpdesk/lib.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 032ce3d7..8b02d47d 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -119,13 +119,14 @@ def send_templated_mail(template_name, sender or settings.DEFAULT_FROM_EMAIL, recipients, bcc=bcc) msg.attach_alternative(html_part, "text/html") - - for filename, filefield in files: - if six.PY3: - msg.attach_file(filefield.path) - else: - with open(filefield.path, 'rb') as attachedfile: - msg.attach(filename, attachedfile.read()) + + if files: + for filename, filefield in files: + if six.PY3: + msg.attach_file(filefield.path) + else: + with open(filefield.path, 'rb') as attachedfile: + msg.attach(filename, attachedfile.read()) return msg.send(fail_silently) From 8f751691a432dd425797fb0b6157781c8239d32c Mon Sep 17 00:00:00 2001 From: Pawel M Date: Fri, 16 Dec 2016 10:38:15 +0100 Subject: [PATCH 18/45] whitespace removed --- helpdesk/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 8b02d47d..1821cc82 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -119,7 +119,7 @@ def send_templated_mail(template_name, 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: From babc7586cf0fc63e66081d166d6c75bbe7b624ac Mon Sep 17 00:00:00 2001 From: pprolancer Date: Sun, 12 Mar 2017 19:58:57 +0330 Subject: [PATCH 19/45] - issue #493: internal error 500 on save query https://github.com/django-helpdesk/django-helpdesk/issues/493 --- helpdesk/views/staff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index 1ed3cd09..4dee2836 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -815,7 +815,7 @@ def ticket_list(request): import json from helpdesk.lib import b64decode try: - query_params = json.loads(b64decode(str(saved_query.query))) + query_params = json.loads(b64decode(str(saved_query.query)).decode()) except ValueError: # Query deserialization failed. (E.g. was a pickled query) return HttpResponseRedirect(reverse('helpdesk:list')) @@ -1112,7 +1112,7 @@ def run_report(request, report): import json from helpdesk.lib import b64decode try: - query_params = json.loads(b64decode(str(saved_query.query))) + query_params = json.loads(b64decode(str(saved_query.query)).decode()) except: return HttpResponseRedirect(reverse('helpdesk:report_index')) From e8d1ffbe21dcd1f4700749f7db94f47046412e2b Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 14 Jun 2017 00:00:31 -0400 Subject: [PATCH 20/45] Fix issue where a django user may not have an associated email address and therefore get_email.py will barf --- helpdesk/management/commands/get_email.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 1c468dca..8d91f27b 100644 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -373,9 +373,10 @@ def ticket_from_message(message, queue, logger): if cc: # get list of currently CC'd emails current_cc = TicketCC.objects.filter(ticket=ticket) - current_cc_emails = [x.email for x in current_cc] - # get emails of any Users CC'd to email - current_cc_users = [x.user.email for x in current_cc] + current_cc_emails = [x.email for x in current_cc if x.email] + # get emails of any Users CC'd to email, if defined + # (some Users may not have an associated email, e.g, when using LDAP) + current_cc_users = [x.user.email for x in current_cc if x.user and x.user.email] # ensure submitter, assigned user, queue email not added other_emails = [queue.email_address] if t.submitter_email: From d264c46385700dd32393597a3dad99e68e23c3e9 Mon Sep 17 00:00:00 2001 From: msaelices Date: Mon, 21 Aug 2017 02:10:43 +0200 Subject: [PATCH 21/45] Fix `UnicodeEncodeError` when ticket has an special char using python2 and user goes to the admin page. --- helpdesk/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index 3b0c605c..621ba223 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -339,6 +339,7 @@ class Queue(models.Model): pass +@python_2_unicode_compatible class Ticket(models.Model): """ To allow a ticket to be entered as quickly as possible, only the From 04a3c3ff5ac0c7c41317ac29355b4215391d0570 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 30 Aug 2017 10:48:16 -0400 Subject: [PATCH 22/45] Update decodestring to decodebytes for Py3 since string name was deprecated in py3.1 --- helpdesk/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index befbacff..ae8d8944 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -11,13 +11,17 @@ import mimetypes import os try: + # Python 2 support from base64 import urlsafe_b64encode as b64encode except ImportError: - from base64 import encodestring as b64encode + # Python 3 support + from base64 import encodebytes as b64encode try: + # Python 2 support from base64 import urlsafe_b64decode as b64decode except ImportError: - from base64 import decodestring as b64decode + # Python 3 support + from base64 import decodebytes as b64decode from django.conf import settings from django.db.models import Q From 1e11e227ff762bbe3af1cc5a608d8dffbf8b3932 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 30 Aug 2017 10:49:56 -0400 Subject: [PATCH 23/45] Update ticket_view for Python 2 and 3, to correct a 500 error in #494 --- helpdesk/views/staff.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/helpdesk/views/staff.py b/helpdesk/views/staff.py index ffe2c0aa..c4044d98 100644 --- a/helpdesk/views/staff.py +++ b/helpdesk/views/staff.py @@ -25,6 +25,8 @@ from django.utils.html import escape from django import forms from django.utils import timezone +from django.utils import six + from helpdesk.forms import ( TicketForm, UserSettingsForm, EmailIgnoreForm, EditTicketForm, TicketCCForm, TicketCCEmailForm, TicketCCUserForm, EditFollowUpForm, TicketDependencyForm @@ -817,7 +819,10 @@ def ticket_list(request): import json from helpdesk.lib import b64decode try: - query_params = json.loads(b64decode(str(saved_query.query)).decode()) + if six.PY3: + query_params = json.loads(b64decode(str(saved_query.query)).decode()) + else: + query_params = json.loads(b64decode(str(saved_query.query))) except ValueError: # Query deserialization failed. (E.g. was a pickled query) return HttpResponseRedirect(reverse('helpdesk:list')) @@ -1114,7 +1119,10 @@ def run_report(request, report): import json from helpdesk.lib import b64decode try: - query_params = json.loads(b64decode(str(saved_query.query)).decode()) + if six.PY3: + query_params = json.loads(b64decode(str(saved_query.query)).decode()) + else: + query_params = json.loads(b64decode(str(saved_query.query))) except: return HttpResponseRedirect(reverse('helpdesk:report_index')) From f5aec2d385f064c6fda6979e73aca0097005d335 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 30 Aug 2017 15:54:19 -0400 Subject: [PATCH 24/45] Update docs for minimum of Django 1.11, set release version to 0.2.0 and change primary maintainer to gwasser --- .travis.yml | 7 ++----- README.rst | 11 ++++++++--- demo/README.rst | 2 +- docs/conf.py | 10 +++++----- docs/index.rst | 6 +++--- docs/install.rst | 2 +- requirements.txt | 2 +- setup.py | 9 +++------ 8 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 04c65710..a8708a05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,15 +2,12 @@ language: python python: - "2.7" - - 3.4.4 + - "3.4" - "3.5" - "3.6" env: - - DJANGO=1.8.18 - - DJANGO=1.9.13 - - DJANGO=1.10.7 - - DJANGO=1.11 + - DJANGO=1.11.4 install: - pip install -q Django==$DJANGO diff --git a/README.rst b/README.rst index 9b5b611d..9fbe07ab 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ django-helpdesk - A Django powered ticket tracker for small businesses. .. image:: https://codecov.io/gh/django-helpdesk/django-helpdesk/branch/master/graph/badge.svg :target: https://codecov.io/gh/django-helpdesk/django-helpdesk -Copyright 2009- Ross Poulton and contributors. All Rights Reserved. +Copyright 2009-2017 Ross Poulton and django-helpdesk contributors. All Rights Reserved. See LICENSE for details. django-helpdesk was formerly known as Jutda Helpdesk, named after the @@ -61,8 +61,13 @@ to alert you to this shortcoming. There is no way around it, sorry. Installation ------------ -django-helpdesk requires either Python 2.7 or 3.4+, as well as Django 1.8+. -The recommended combination is Python 3.4+ with Django 1.10. +django-helpdesk requires either Python 2.7 or 3.4+, as well as Django 1.11+. + +**NOTE REGARDING PYTHON VERSION:** +The recommended combination is Python 3.4+ with Django 1.11+. +Support for Python 2 will end in the next versions of both django-helpdesk +and Django itself (2.0), so users and developers are encouraged to begin +transitioning to Python 3 if have not already. You can quickly install the latest stable version of django-helpdesk app via pip: diff --git a/demo/README.rst b/demo/README.rst index 548bb850..14bcfd0d 100644 --- a/demo/README.rst +++ b/demo/README.rst @@ -73,7 +73,7 @@ Then navigate to the site in a browser as above. *NOTE ON DJANGO VERISON* -The demo project was also created with Django 1.10 +The demo project was also created with Django 1.11 in mind. If you are using a different version of Django, slight tweaks might be necessary to make the demo work. diff --git a/docs/conf.py b/docs/conf.py index 7f441650..6ecfce4a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,16 +41,16 @@ master_doc = 'index' # General information about the project. project = u'django-helpdesk' -copyright = u'2011, Ross Poulton + Contributors' +copyright = u'2011-2017, Ross Poulton + django-helpdesk Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = '0.2' # The full version, including alpha/beta/rc tags. -release = '0.1' +release = '0.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -179,7 +179,7 @@ htmlhelp_basename = 'django-helpdeskdoc' # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'django-helpdesk.tex', u'django-helpdesk Documentation', - u'Ross Poulton + Contributors', 'manual'), + u'Ross Poulton + django-helpdesk Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -212,5 +212,5 @@ latex_documents = [ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'django-helpdesk', u'django-helpdesk Documentation', - [u'Ross Poulton + Contributors'], 1) + [u'Ross Poulton + django-helpdesk Contributors'], 1) ] diff --git a/docs/index.rst b/docs/index.rst index ae9e3ead..37054149 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,13 +10,13 @@ Contents :maxdepth: 2 :glob: - license install configuration settings spam custom_fields contributing + license How Does It Look? @@ -69,7 +69,7 @@ django-helpdesk is released under the BSD license, however it packages 3rd party Dependencies ------------ -1. Python 2.7+ (or 3.4+) -2. Django (1.8 or newer) +1. Python 3.4+ (or 2.7, but deprecated and support will be removed next release) +2. Django 1.11 or newer 3. An existing **working** Django project with database etc. If you cannot log into the Admin, you won't get this product working! This means you **must** run `syncdb` **before** you add ``helpdesk`` to your ``INSTALLED_APPS``. diff --git a/docs/install.rst b/docs/install.rst index 26d7a36e..ea0aa77f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -63,7 +63,7 @@ errors with trying to create User settings. This line will have to come *after* any other lines in your urls.py such as those used by the Django admin. - Note that the `helpdesk` namespace is no longer required for Django 1.9 and you can use a different namespace. + Note that the `helpdesk` namespace is no longer required for Django 1.9+ and you can use a different namespace. However, it is recommended to use the default namespace name for clarity. 3. Create the required database tables. diff --git a/requirements.txt b/requirements.txt index 2b53215d..0b95046e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=1.8 +Django>=1.11 django-bootstrap-form>=3.1,<4 email-reply-parser django-markdown-deux diff --git a/setup.py b/setup.py index 9d1f8d03..39602eb3 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.util import convert_path from fnmatch import fnmatchcase from setuptools import setup, find_packages -version = '0.2.0.1' +version = '0.2.0' # Provided as an attribute, so you can append to these instead # of replicating them: @@ -133,9 +133,6 @@ setup( "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Framework :: Django", - "Framework :: Django :: 1.8", - "Framework :: Django :: 1.9", - "Framework :: Django :: 1.10", "Framework :: Django :: 1.11", "Environment :: Web Environment", "Operating System :: OS Independent", @@ -150,8 +147,8 @@ setup( 'cases', 'bugs', 'track', 'support'], author='Ross Poulton', author_email='ross@rossp.org', - maintainer='Jonathan Barratt', - maintainer_email='jonathan@the-im.com', + maintainer='Garret Wassermann', + maintainer_email='gwasser@gmail.com', url='https://github.com/django-helpdesk/django-helpdesk', license='BSD', packages=find_packages(), From b99b55e42664860c496c86a3bb1c0ce2377f80d0 Mon Sep 17 00:00:00 2001 From: Will Stott Date: Wed, 6 Sep 2017 16:22:06 +0100 Subject: [PATCH 25/45] Miscellaneous debug log additions. --- helpdesk/lib.py | 11 ++++++++++- helpdesk/management/commands/get_email.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/helpdesk/lib.py b/helpdesk/lib.py index 10f4eb00..828a70c6 100644 --- a/helpdesk/lib.py +++ b/helpdesk/lib.py @@ -9,6 +9,7 @@ lib.py - Common functions (eg multipart e-mail) import logging import mimetypes import os +from smtplib import SMTPException try: # Python 2 support @@ -135,7 +136,15 @@ def send_templated_mail(template_name, content = attachedfile.read() msg.attach(filename, content) - return msg.send(fail_silently) + logger.debug('Sending email to: {!r}'.format(recipients)) + + try: + return msg.send() + except SMTPException: + logger.exception('SMTPException raised while sending email to {}'.format(recipients)) + if not fail_silently: + raise e + return 0 def query_to_dict(results, descriptions): diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 8f0a4661..4b979fcc 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -447,7 +447,7 @@ def ticket_from_message(message, queue, logger): attached = process_attachments(f, files) for att_file in attached: - logger.info("Attachment '%s' successfully added to ticket from email." % att_file[0]) + logger.info("Attachment '%s' (with size %s) successfully added to ticket from email." % (att_file[0], att_file[1].size)) context = safe_template_context(t) From c69f752f1c41402be0b8c6c50e06243d335b997f Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Tue, 12 Sep 2017 20:57:10 -0400 Subject: [PATCH 26/45] Django 2.0 requires explicit parameters; must go back and change others --- helpdesk/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/helpdesk/models.py b/helpdesk/models.py index 621ba223..2c2fbcf5 100644 --- a/helpdesk/models.py +++ b/helpdesk/models.py @@ -259,6 +259,7 @@ class Queue(models.Model): blank=True, null=True, verbose_name=_('Default owner'), + on_delete=models.SET_NULL, ) def __str__(self): From a5d5ef0bc5a36a89dd0e11af8d4040d05e2fe663 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Tue, 12 Sep 2017 21:16:30 -0400 Subject: [PATCH 27/45] We require specifically Django 1.11.x, update documentation --- CONTRIBUTING.rst | 165 ++++++++++++++++++++++++++--------------------- README.rst | 53 ++++++++------- demo/README.rst | 4 +- requirements.txt | 2 +- 4 files changed, 127 insertions(+), 97 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 53182db0..1b81569a 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -13,18 +13,105 @@ https://github.com/django-helpdesk/django-helpdesk Licensing --------- -All contributions to django-helpdesk must be under the BSD license documented in -the LICENSE file in the top-level directory of this project. +All contributions to django-helpdesk *must* be under the BSD license documented +in the LICENSE file in the top-level directory of this project. By submitting a contribution to this project (in any way: via e-mail, -via GitHub forks, attachments, etc), you acknowledge that your contribution is -open-source and licensed under the BSD license. +via GitHub pull requests, ticket attachments, etc), you acknowledge that your +contribution is open-source and licensed under the BSD license. If you or your organisation does not accept these license terms then we cannot accept your contribution. Please reconsider! -Translations ------------- +Ways to Contribute +------------------ + +We're happy to include any type of contribution! This can be: +* back-end python/django code development +* front-end web development (HTML/Javascript, especially jQuery) +* language translations +* writing improved documentation and demos + +More details on each of theses tasks is below. + +If you have any questions on contributing, please start a discussion on +the GitHub issue tracker at + +https://github.com/django-helpdesk/django-helpdesk/issues + +### Back-end Python/Django + +Please fork the project on GitHub, make your changes, and submit a +pull request back into the appropriate branch of the +django-helpdesk repository. + +In general, we use the following git branching scheme: +* `master` always refers to development for the next major release +* major releases are found in their own branches +** `0.2.x` is the branch for the 0.2 release and any bugfix releases +** `0.1` is the branch for the legacy code; it is no longer supported + +If you are submitting a patch for a 0.2 release, apply your pull request +to the `0.2.x` branch. If you are helping develop the next release, +apply your patches to the `master` branch. + +Wherever possible please break git commits up into small chunks that are +specific to a single bit of functionality. For example, a commit should *not* +contain both new functionality *and* a bugfix; the new function and the bugfix +should be separate commits wherever possible. + +Commit messages should also explain *what*, precisely, has been changed. + +All commits should include appropriate new or updated tests; see the Tests +section below for more details. + +If your changes affect the Django models for django-helpdesk, be aware +that your commits should include database schema python scripts; see the +Database Schema Changes section below for more details. + +#### Tests + +Currently, test coverage is very low. We're working on increasing this, and to +make life easier we are using Travis CI (http://travis-ci.org/) for continuous +integration. This means that the test suite is run every time a code change is +made, so we can try and make sure we avoid basic bugs and other regressions. + +As a general policy, we will only accept new feature commits if they are +accompanied by appropriate unit/functional tests (that is, tests for the +functionality you just added). Bugfixes should also include new unit tests to +ensure the bug has been fixed. + +More significant code refactoring must also include proper integration or +validation tests, to be committed *BEFORE* the refactoring patches. This is to +ensure that the refactored code produces the same results as the previous code +base. + +Any further integration or validation tests (tests for the entire +django-helpdesk application) are not required but greatly appreciated until we +can improve our overall test coverage. + +Please include tests in the ``tests/`` folder when committing code changes. + +If you have any questions about creating or maintaining proper tests, please +start a discussion on the GitHub issue tracker at + +https://github.com/django-helpdesk/django-helpdesk/issues + +#### Database schema changes + +As well as making your normal code changes to ``models.py``, please generate a +Django migration file and commit it with your code. You will want to use a +command similar to the following:: + + ./manage.py migrate helpdesk --auto [migration_name] + +Make sure that ``migration_name`` is a sensible single-string explanation of +what this migration does, such as *add_priority_options* or *add_basket_table*. + +This will add a file to the ``migrations/`` folder, which must be committed to +git with your other code changes. + +### Translations Although django-helpdesk has originally been written for the English language, there are already multiple translations to Spanish, Polish, German and Russian. @@ -40,69 +127,3 @@ http://www.transifex.net/projects/p/django-helpdesk/resource/core/ Once you have translated content via Transifex, please raise an issue on the project Github page to let us know it's ready to import. - -Code changes ------------- - -Please fork the project on GitHub, make your changes, and log a pull request to -get the changes pulled back into the -master branch of the django-helpdesk repository. - -Wherever possible please break git commits up into small chunks that are -specific to a single bit of functionality. For example, a commit should not -contain both new functionality *and* a bugfix; the new function and the bugfix -should be separate commits wherever possible. - -Commit messages should also explain *what*, precisely, has been changed. - -All commits should include appropriate new or updated tests; see the Tests -section below for more details. - -If you have any questions, please start a discussion on the GitHub issue tracker -at - -https://github.com/django-helpdesk/django-helpdesk/issues - -Tests ------ - -Currently, test coverage is very low. We're working on increasing this, and to -make life easier we are using `Travis CI` (http://travis-ci.org/) for continuous -integration. This means that the test suite is run every time a code change is -made, so we can try and make sure we avoid basic bugs and other regressions. - -As a general policy, we will only accept new feature commits if they are -accompanied by appropriate unit/functional tests (that is, tests for the -functionality you just added). Bugfixes should also include new unit tests to -ensure the bug has been fixed. - -More significant code refactoring must also include proper integration or -validation tests, to be committed BEFORE the refactoring patches. This is to -ensure that the refactored code produces the same results as the previous code -base. - -Any further integration or validation tests (tests for the entire -django-helpdesk application) are not required but greatly appreciated until we -can improve our overall test coverage. - -Please include tests in the ``tests/`` folder when committing code changes. - -If you have any questions about creating or maintaining proper tests, please -start a discussion on the GitHub issue tracker at - -https://github.com/django-helpdesk/django-helpdesk/issues - -Database schema changes ------------------------ - -As well as making your normal code changes to ``models.py``, please generate a -Django migration file and commit it with your code. You will want to use a -command similar to the following:: - - ./manage.py migrate helpdesk --auto [migration_name] - -Make sure that ``migration_name`` is a sensible single-string explanation of -what this migration does, such as *add_priority_options* or *add_basket_table*. - -This will add a file to the ``migrations/`` folder, which must be committed to -git with your other code changes. diff --git a/README.rst b/README.rst index 9fbe07ab..062d8b2a 100644 --- a/README.rst +++ b/README.rst @@ -36,55 +36,60 @@ Demo Quickstart django-helpdesk includes a basic demo Django project so that you may easily get started with testing or developing django-helpdesk. The demo project -resides in the demo/ top-level folder. +resides in the `demo/` top-level folder. It's likely that you can start up a demo project server by running -only the command: +only the command:: sudo make rundemo -then pointing your web browser at localhost:8080. +then pointing your web browser at `localhost:8080`. -For more information and options, please read the demo/README.rst file. +For more information and options, please read the `demo/README.rst` file. **NOTE REGARDING SQLITE AND SEARCHING:** -The demo project uses sqlite as its database. Sqlite does not allow +The demo project uses `sqlite` as its database. Sqlite does not allow case-insensitive searches and so the search function may not work as effectively as it would on other database such as PostgreSQL or MySQL that does support case-insensitive searches. -For more information, see this note in the Django documentation: -http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching +For more information, see [this note in the Django documentation](http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching). -When you try to do a keyword search using sqlite, a message will be displayed +When you try to do a keyword search using `sqlite`, a message will be displayed to alert you to this shortcoming. There is no way around it, sorry. Installation ------------ -django-helpdesk requires either Python 2.7 or 3.4+, as well as Django 1.11+. +`django-helpdesk` requires: +* Django 1.11.x *only* +* either Python 2.7 or 3.4+ **NOTE REGARDING PYTHON VERSION:** -The recommended combination is Python 3.4+ with Django 1.11+. -Support for Python 2 will end in the next versions of both django-helpdesk -and Django itself (2.0), so users and developers are encouraged to begin -transitioning to Python 3 if have not already. +The recommended combination is Python 3.4+ with Django 1.11. +Support for Python 2 will end in the next versions of both `django-helpdesk` +and Django itself (Django 2.0), so users and developers are encouraged to begin +transitioning to Python 3 if have not already. New projects should definitely +use Python 3! -You can quickly install the latest stable version of django-helpdesk app via pip: +You can quickly install the latest stable version of `django-helpdesk` +app via `pip`:: pip install django-helpdesk -You may also check out the master branch on GitHub, and install manually: +You may also check out the `master` branch on GitHub, and install manually:: python setup.py install -Either way, you will need to add django-helpdesk to an existing Django project. +Either way, you will need to add `django-helpdesk` to an existing +Django project. -For further installation information see docs/install.html and docs/configuration.html +For further installation information see `docs/install.html` +and `docs/configuration.html` Upgrading from previous versions -------------------------------- -If you are upgrading from a previous version of django-helpdesk that used +If you are upgrading from a previous version of `django-helpdesk` that used migrations, get an up to date version of the code base (eg by using `git pull` or `pip install --upgrade django-helpdesk`) then migrate the database:: @@ -95,15 +100,17 @@ Lastly, restart your web server software (eg Apache) or FastCGI instance, to ensure the latest changes are in use. Unfortunately we are unable to assist if you are upgrading from a -version of django-helpdesk prior to migrations (ie pre-2011). +version of `django-helpdesk` prior to migrations (ie pre-2011). You can continue to the 'Initial Configuration' area, if needed. Contributing ------------ -We're happy to include any type of contribution! This can be back-end -python/django code development, front-end web development -(HTML/Javascript, especially jQuery), or even language translations. +We're happy to include any type of contribution! This can be: +* back-end python/django code development +* front-end web development (HTML/Javascript, especially jQuery) +* language translations +* writing improved documentation and demos -For more information on contributing, please see the CONTRIBUTING.rst file. +For more information on contributing, please see the `CONTRIBUTING.rst` file. diff --git a/demo/README.rst b/demo/README.rst index 14bcfd0d..52d980bd 100644 --- a/demo/README.rst +++ b/demo/README.rst @@ -74,8 +74,10 @@ Then navigate to the site in a browser as above. *NOTE ON DJANGO VERISON* The demo project was also created with Django 1.11 -in mind. If you are using a different version of Django, +in mind. If you are using an older version of Django, slight tweaks might be necessary to make the demo work. +Please remember that we do not currently support any +version of Django other than 1.11. *NOTE ON ATTACHMENTS* diff --git a/requirements.txt b/requirements.txt index 0b95046e..7b9b0192 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django>=1.11 +Django=1.11 django-bootstrap-form>=3.1,<4 email-reply-parser django-markdown-deux From a50318b1308b531db2fe6c22b174725acfb7b002 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Tue, 12 Sep 2017 21:58:22 -0400 Subject: [PATCH 28/45] Fix bug in requiring django==1.11 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7b9b0192..0bccf59c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django=1.11 +Django==1.11 django-bootstrap-form>=3.1,<4 email-reply-parser django-markdown-deux From 11902cc9c811cf955b1d538e06067fb6f0cedf9f Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 00:58:00 -0400 Subject: [PATCH 29/45] Fix .rst markup, accidentally used .md --- CONTRIBUTING.rst | 9 ++++++--- README.rst | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1b81569a..4a706822 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -27,6 +27,7 @@ Ways to Contribute ------------------ We're happy to include any type of contribution! This can be: + * back-end python/django code development * front-end web development (HTML/Javascript, especially jQuery) * language translations @@ -46,10 +47,12 @@ pull request back into the appropriate branch of the django-helpdesk repository. In general, we use the following git branching scheme: + * `master` always refers to development for the next major release -* major releases are found in their own branches -** `0.2.x` is the branch for the 0.2 release and any bugfix releases -** `0.1` is the branch for the legacy code; it is no longer supported +* major releases are found in their own branches: + + * `0.2.x` is the branch for the 0.2 release and any bugfix releases + * `0.1` is the branch for the legacy code; it is no longer supported If you are submitting a patch for a 0.2 release, apply your pull request to the `0.2.x` branch. If you are helping develop the next release, diff --git a/README.rst b/README.rst index 062d8b2a..4b1822c8 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ The demo project uses `sqlite` as its database. Sqlite does not allow case-insensitive searches and so the search function may not work as effectively as it would on other database such as PostgreSQL or MySQL that does support case-insensitive searches. -For more information, see [this note in the Django documentation](http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching). +For more information, see this note_ in the Django documentation. When you try to do a keyword search using `sqlite`, a message will be displayed to alert you to this shortcoming. There is no way around it, sorry. @@ -61,6 +61,7 @@ Installation ------------ `django-helpdesk` requires: + * Django 1.11.x *only* * either Python 2.7 or 3.4+ @@ -108,9 +109,12 @@ Contributing ------------ We're happy to include any type of contribution! This can be: + * back-end python/django code development * front-end web development (HTML/Javascript, especially jQuery) * language translations * writing improved documentation and demos For more information on contributing, please see the `CONTRIBUTING.rst` file. + +.. _note: http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching From b65042bf41b61be67c2673df90500eedf988a0a7 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 01:04:51 -0400 Subject: [PATCH 30/45] More .rst changes --- CONTRIBUTING.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4a706822..99da7f7f 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -40,7 +40,8 @@ the GitHub issue tracker at https://github.com/django-helpdesk/django-helpdesk/issues -### Back-end Python/Django +Back-end Python/Django +`````````````````````` Please fork the project on GitHub, make your changes, and submit a pull request back into the appropriate branch of the @@ -72,7 +73,8 @@ If your changes affect the Django models for django-helpdesk, be aware that your commits should include database schema python scripts; see the Database Schema Changes section below for more details. -#### Tests +Tests +..... Currently, test coverage is very low. We're working on increasing this, and to make life easier we are using Travis CI (http://travis-ci.org/) for continuous @@ -100,7 +102,8 @@ start a discussion on the GitHub issue tracker at https://github.com/django-helpdesk/django-helpdesk/issues -#### Database schema changes +Database schema changes +....................... As well as making your normal code changes to ``models.py``, please generate a Django migration file and commit it with your code. You will want to use a @@ -114,7 +117,8 @@ what this migration does, such as *add_priority_options* or *add_basket_table*. This will add a file to the ``migrations/`` folder, which must be committed to git with your other code changes. -### Translations +Translations +```````````` Although django-helpdesk has originally been written for the English language, there are already multiple translations to Spanish, Polish, German and Russian. From 478095ca7e51de2d0341b62828a23e3763bebe58 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 01:17:05 -0400 Subject: [PATCH 31/45] Fix missing templatetag load, to address #549 --- helpdesk/templates/helpdesk/ticket_desc_table.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/templates/helpdesk/ticket_desc_table.html b/helpdesk/templates/helpdesk/ticket_desc_table.html index fb2479e5..bf9281dc 100644 --- a/helpdesk/templates/helpdesk/ticket_desc_table.html +++ b/helpdesk/templates/helpdesk/ticket_desc_table.html @@ -1,4 +1,4 @@ -{% load i18n humanize %} +{% load i18n humanize ticket_to_link %} {% load static from staticfiles %}
From 5846732ef166bb803c42afa2056256eb67867642 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 01:23:17 -0400 Subject: [PATCH 32/45] Better requirements to allow any bugfix of Django 1.11 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0bccf59c..a17d8719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==1.11 +Django>=1.11,<2 django-bootstrap-form>=3.1,<4 email-reply-parser django-markdown-deux From 96cae9bdd5132d5a4468d9d18c647bd2431e9f03 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 01:44:36 -0400 Subject: [PATCH 33/45] Added better logging for case when IMAP folder may not exist when check IMAP messages, to address issue in #536 --- helpdesk/management/commands/get_email.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 4b979fcc..476c7d89 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -195,7 +195,10 @@ def process_queue(q, logger): settings.QUEUE_EMAIL_BOX_PASSWORD) server.select(q.email_box_imap_folder) - status, data = server.search(None, 'NOT', 'DELETED') + try: + status, data = server.search(None, 'NOT', 'DELETED') + except imaplib.IMAP4.error: + logger.error("IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?" % q.email_box_imap_folder) if data: msgnums = data[0].split() logger.info("Received %d messages from IMAP server" % len(msgnums)) From dc5bcf53e804f5184fe66000461e86f2da85e2ef Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Wed, 13 Sep 2017 02:42:42 -0400 Subject: [PATCH 34/45] pytz is required by django 1.11 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a17d8719..50c42ca0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ django-bootstrap-form>=3.1,<4 email-reply-parser django-markdown-deux simplejson +pytz six From 56f32022af0451fa37a66a631f8acee57195a166 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 00:37:00 -0400 Subject: [PATCH 35/45] Add dependency info and explicit warning of dropping py2 support to the install docs --- docs/install.rst | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index ea0aa77f..00735f42 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,7 +1,27 @@ Installation ============ -django-helpdesk installation isn't difficult, but it requires you have a bit of existing know-how about Django. +``django-helpdesk`` installation isn't difficult, but it requires you have a bit of existing know-how about Django. + + +Prerequisites +------------- + +Before getting started, ensure your system meets the following dependencies: + +* Python 3.4+, or Python 2.7 +* Django 1.11.x (Django 2.0 support is coming in a future release; older + releases such as 1.8-1.10 *may* work, but are not guaranteed. Django's + deprecation policy suggests that any project that worked with 1.8 should + be able to upgrade to 1.11 without any problems) + +Ensure any extra Django modules you wish to use are compatible before continuing. + +**NOTE**: Python 2.7 support is deprecated in both ``django-helpdesk`` and Django. +Future releases of ``django-helpdesk`` may remove support for Python 2.7, +and Django will no longer support Python 2.7 as of the Django 2.0 release. +It is highly recommended to start new projects using Python 3.4+, or migrate +existing projects to Python 3.4+. Getting The Code @@ -12,10 +32,10 @@ Installing using PIP Try using ``pip install django-helpdesk``. Go and have a beer to celebrate Python packaging. -GIT Checkout (Cutting Edge) -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Checkout ``master`` from git (Cutting Edge) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git`` +If you're planning on editing the code or just want to get whatever is the latest and greatest, you can clone the official Git repository with ``git clone git://github.com/django-helpdesk/django-helpdesk.git``. We use the ``master`` branch as our development branch for the next major release of ``django-helpdesk``. Copy the ``helpdesk`` folder into your ``PYTHONPATH``. @@ -124,7 +144,7 @@ Upgrading from previous versions If you are upgrading from a previous version of django-helpdesk that used migrations, get an up to date version of the code base (eg by using -`git pull` or `pip install --upgrade django-helpdesk`) then migrate the database:: +``git pull`` or ``pip install --upgrade django-helpdesk``) then migrate the database:: python manage.py migrate helpdesk --db-dry-run # DB untouched python manage.py migrate helpdesk From 335e1db35e43b9bca6b637e3062cde3fd863f2ea Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 01:59:37 -0400 Subject: [PATCH 36/45] Update MANIFEST for new files in 0.2.0 and demo --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index f7e76ad8..92aabae8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,13 @@ include README.rst +include CONTRIBUTING.rst include AUTHORS include LICENSE* include requirements.txt +include requirements-testing.txt recursive-include helpdesk/static/helpdesk * recursive-include helpdesk/locale *.po *.mo recursive-include helpdesk/templates * recursive-include helpdesk/fixtures *.json recursive-include docs/html * +recursive-include demo * From 1b8a230cf1df0df3ab567c821a05c14dc06cf3ab Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 02:19:45 -0400 Subject: [PATCH 37/45] Missing generic python classifiers in setup.py --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 39602eb3..ea3e356a 100644 --- a/setup.py +++ b/setup.py @@ -128,7 +128,9 @@ setup( classifiers=[ "Development Status :: 4 - Beta", "Programming Language :: Python", + "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", From a7ae0a178ebd69fe7162a3e2e7874e8d2151dfa3 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 04:19:10 -0400 Subject: [PATCH 38/45] django-bootstrap-form 3.3 or above required for django 1.11 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 50c42ca0..5e34629e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Django>=1.11,<2 -django-bootstrap-form>=3.1,<4 +django-bootstrap-form>=3.3,<4 email-reply-parser django-markdown-deux simplejson From a6d2a3adeaa8b7620cafefb0dd168645b543f6ff Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 04:41:53 -0400 Subject: [PATCH 39/45] bump version to 0.2.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ea3e356a..51c61877 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.util import convert_path from fnmatch import fnmatchcase from setuptools import setup, find_packages -version = '0.2.0' +version = '0.2.1' # Provided as an attribute, so you can append to these instead # of replicating them: From 3501fcc1c87d20c29c1421782cf3168ea82a12db Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 14 Sep 2017 04:47:28 -0400 Subject: [PATCH 40/45] master is now 0.3 development --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 51c61877..83aa9e00 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from distutils.util import convert_path from fnmatch import fnmatchcase from setuptools import setup, find_packages -version = '0.2.1' +version = '0.3.0' # Provided as an attribute, so you can append to these instead # of replicating them: From 252cc705c9c697818eb99cd8de0e0f07df411560 Mon Sep 17 00:00:00 2001 From: zodman Date: Tue, 26 Sep 2017 16:38:25 -0500 Subject: [PATCH 41/45] fix make rundemo2 --- demo/demodesk/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 demo/demodesk/__init__.py diff --git a/demo/demodesk/__init__.py b/demo/demodesk/__init__.py new file mode 100644 index 00000000..e69de29b From 540531c916814428a937280798a10806e0099250 Mon Sep 17 00:00:00 2001 From: zodman Date: Tue, 26 Sep 2017 17:06:34 -0500 Subject: [PATCH 42/45] fix css --- helpdesk/static/helpdesk/helpdesk-extend.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helpdesk/static/helpdesk/helpdesk-extend.css b/helpdesk/static/helpdesk/helpdesk-extend.css index e2c0e59a..4d280241 100644 --- a/helpdesk/static/helpdesk/helpdesk-extend.css +++ b/helpdesk/static/helpdesk/helpdesk-extend.css @@ -50,7 +50,10 @@ Bootstrap overrides #searchtabs {margin-bottom: 20px;} .row_tablehead, table.table caption {background-color: #dbd5d9;} -table.table caption {height: 2em; line-height: 2em; font-weight: bold;} +table.table caption { + padding-left: 2em; + line-height: 2em; font-weight: bold; +} table.ticket-stats caption {color: #fbff00; font-style: italic;} table.ticket-stats tbody th, table.ticket-stats tbody tr {padding-left: 20px} From 03a57bdc5fe412747eb9ea251b649691483ee827 Mon Sep 17 00:00:00 2001 From: zodman Date: Wed, 27 Sep 2017 12:40:47 -0500 Subject: [PATCH 43/45] fix get_email from gmail imap --- demo/demodesk/config/settings.py | 5 +++++ helpdesk/management/commands/get_email.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index 24b9eccc..070f91cb 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -207,3 +207,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # - This is only necessary to make the demo project work, not needed for # your own projects unless you make your own fixtures FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures')] + +try: + from .local_settings import * +except ImportError: + pass diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 476c7d89..5b2c2036 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -340,13 +340,14 @@ def ticket_from_message(message, queue, logger): if not name: ext = mimetypes.guess_extension(part.get_content_type()) name = "part-%i%s" % (counter, ext) - payload = part.get_payload() + if isinstance(payload, list): + payload = payload.pop().as_string() payloadToWrite = payload try: logger.debug("Try to base64 decode the attachment payload") payloadToWrite = base64.decodestring(payload) - except binascii.Error: + except binascii.Error, TypeError: logger.debug("Payload was not base64 encoded, using raw bytes") payloadToWrite = payload files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) From f06cbce07c4ed161e6b75c501f066295fd5c391a Mon Sep 17 00:00:00 2001 From: zodman Date: Wed, 27 Sep 2017 12:50:51 -0500 Subject: [PATCH 44/45] fix the python3 --- helpdesk/management/commands/get_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 5b2c2036..40c25b1f 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -347,7 +347,7 @@ def ticket_from_message(message, queue, logger): try: logger.debug("Try to base64 decode the attachment payload") payloadToWrite = base64.decodestring(payload) - except binascii.Error, TypeError: + except (binascii.Error, TypeError): logger.debug("Payload was not base64 encoded, using raw bytes") payloadToWrite = payload files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) From b48f72ff112383c29ecae791e748ca037e17e1d9 Mon Sep 17 00:00:00 2001 From: Garret Wassermann Date: Thu, 28 Sep 2017 01:08:54 -0400 Subject: [PATCH 45/45] Merge fixes from master --- demo/demodesk/__init__.py | 0 demo/demodesk/config/settings.py | 5 +++++ helpdesk/management/commands/get_email.py | 5 +++-- helpdesk/static/helpdesk/helpdesk-extend.css | 5 ++++- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 demo/demodesk/__init__.py diff --git a/demo/demodesk/__init__.py b/demo/demodesk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/demo/demodesk/config/settings.py b/demo/demodesk/config/settings.py index 24b9eccc..070f91cb 100644 --- a/demo/demodesk/config/settings.py +++ b/demo/demodesk/config/settings.py @@ -207,3 +207,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # - This is only necessary to make the demo project work, not needed for # your own projects unless you make your own fixtures FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures')] + +try: + from .local_settings import * +except ImportError: + pass diff --git a/helpdesk/management/commands/get_email.py b/helpdesk/management/commands/get_email.py index 476c7d89..40c25b1f 100755 --- a/helpdesk/management/commands/get_email.py +++ b/helpdesk/management/commands/get_email.py @@ -340,13 +340,14 @@ def ticket_from_message(message, queue, logger): if not name: ext = mimetypes.guess_extension(part.get_content_type()) name = "part-%i%s" % (counter, ext) - payload = part.get_payload() + if isinstance(payload, list): + payload = payload.pop().as_string() payloadToWrite = payload try: logger.debug("Try to base64 decode the attachment payload") payloadToWrite = base64.decodestring(payload) - except binascii.Error: + except (binascii.Error, TypeError): logger.debug("Payload was not base64 encoded, using raw bytes") payloadToWrite = payload files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0])) diff --git a/helpdesk/static/helpdesk/helpdesk-extend.css b/helpdesk/static/helpdesk/helpdesk-extend.css index e2c0e59a..4d280241 100644 --- a/helpdesk/static/helpdesk/helpdesk-extend.css +++ b/helpdesk/static/helpdesk/helpdesk-extend.css @@ -50,7 +50,10 @@ Bootstrap overrides #searchtabs {margin-bottom: 20px;} .row_tablehead, table.table caption {background-color: #dbd5d9;} -table.table caption {height: 2em; line-height: 2em; font-weight: bold;} +table.table caption { + padding-left: 2em; + line-height: 2em; font-weight: bold; +} table.ticket-stats caption {color: #fbff00; font-style: italic;} table.ticket-stats tbody th, table.ticket-stats tbody tr {padding-left: 20px}