From d76fa2c71ece026e6d00d532b53bd2ecc269e6b5 Mon Sep 17 00:00:00 2001 From: Pawel M Date: Thu, 8 Dec 2016 09:32:04 +0100 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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: