Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
gwasser committed Dec 14, 2020
2 parents 662f571 + 959fc0f commit 03c6a50
Show file tree
Hide file tree
Showing 114 changed files with 15,061 additions and 76 deletions.
25 changes: 25 additions & 0 deletions LICENSE.3RDPARTY
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ distributed with django-helpdesk.
10. License for Metis Menu
11. License for Bootstrap CSS v4.2.1
12. License for Font Awesome v5.6.3
13. License for Timeline 3

----------------------------------------------------------------------

Expand Down Expand Up @@ -330,4 +331,28 @@ trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

----------------------------------------------------------------------

13. License for Timeline 3

TimelineJS is released under the Mozilla Public License (MPL), version 2.0.
That means that TimelineJS is free to "use, reproduce, make available, modify,
display, perform, distribute" or otherwise employ. You don't need our permission
to publish stories with TimelineJS and you don't need to pay us any fees or
arrange any further license beyond the MPL. You may also make changes to our
source code, although if you use changed code publicly, you must make the source
code to your modified version available.

Note that this page is not offered as legal counsel and you may wish to consult
your own lawyers.

If you have more questions, feel free to reach out to us.

You may:
embed TimelineJS in a personal or commercial website.
receive compensation for a page containing Timelines you have created, whether
from a client or from display advertising.
copy, modify, clone or fork the code for personal or commercial use.
host the TimelineJS source code on your own server.


6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ Django project.
For further installation information see `docs/install.html`
and `docs/configuration.html`

Testing
-------

See quicktest.py for usage details

Upgrading from previous versions
--------------------------------

Expand Down Expand Up @@ -111,3 +116,4 @@ We're happy to include any type of contribution! This can be:
For more information on contributing, please see the `CONTRIBUTING.rst` file.

.. _note: http://docs.djangoproject.com/en/dev/ref/databases/#sqlite-string-matching

130 changes: 78 additions & 52 deletions helpdesk/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,35 @@
(c) Copyright 2008 Jutda. Copyright 2018 Timothy Hobbs. All Rights Reserved.
See LICENSE for details.
"""
from django.core.exceptions import ValidationError
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, timezone
from django.contrib.auth import get_user_model

from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail

from datetime import timedelta
import base64
import binascii
# import base64
import email
from email.header import decode_header
from email.utils import getaddresses, parseaddr, collapse_rfc2231_value
import imaplib
import logging
import mimetypes
from os import listdir, unlink
from os.path import isfile, join
import os
import poplib
import re
import socket
import ssl
import sys
from datetime import timedelta
from email.utils import getaddresses
from os.path import isfile, join
from time import ctime
from optparse import make_option

from bs4 import BeautifulSoup

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Q
from django.utils import encoding, timezone
from django.utils.translation import ugettext as _
from email_reply_parser import EmailReplyParser

import logging
from helpdesk import settings
from helpdesk.lib import safe_template_context, process_attachments
from helpdesk.models import Queue, Ticket, TicketCC, FollowUp, IgnoreEmail


# import User model, which may be a custom model
User = get_user_model()
Expand Down Expand Up @@ -70,37 +63,48 @@ def process_email(quiet=False):
if q.logging_type in logging_types:
logger.setLevel(logging_types[q.logging_type])
elif not q.logging_type or q.logging_type == 'none':
logging.disable(logging.CRITICAL) # disable all messages
# disable all handlers so messages go to nowhere
logger.handlers = []
logger.propagate = False
if quiet:
logger.propagate = False # do not propagate to root logger that would log to console
logdir = q.logging_dir or '/var/log/helpdesk/'

try:
handler = logging.FileHandler(join(logdir, q.slug + '_get_email.log'))
logger.addHandler(handler)
# Log messages to specific file only if the queue has it configured
if (q.logging_type in logging_types) and q.logging_dir: # if it's enabled and the dir is set
log_file_handler = logging.FileHandler(join(q.logging_dir, q.slug + '_get_email.log'))
logger.addHandler(log_file_handler)
else:
log_file_handler = None

try:
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()
finally:
# we must close the file handler correctly if it's created
try:
handler.close()
if log_file_handler:
log_file_handler.close()
except Exception as e:
logging.exception(e)
try:
logger.removeHandler(handler)
if log_file_handler:
logger.removeHandler(log_file_handler)
except Exception as e:
logging.exception(e)


def pop3_sync(q, logger, server):
server.getwelcome()
try:
server.stls()
except Exception:
logger.warning("POP3 StartTLS failed or unsupported. Connection will be unencrypted.")
server.user(q.email_box_user or settings.QUEUE_EMAIL_BOX_USER)
server.pass_(q.email_box_pass or settings.QUEUE_EMAIL_BOX_PASSWORD)

Expand Down Expand Up @@ -138,17 +142,27 @@ def pop3_sync(q, logger, server):

def imap_sync(q, logger, server):
try:
try:
server.starttl()
except Exception:
logger.warning("IMAP4 StartTLS unsupported or failed. Connection will be unencrypted.")
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)
except imaplib.IMAP4.abort:
logger.error("IMAP login failed. Check that the server is accessible and that the username and password are correct.")
logger.error(
"IMAP login failed. Check that the server is accessible and that "
"the username and password are correct."
)
server.logout()
sys.exit()
except ssl.SSLError:
logger.error("IMAP login failed due to SSL error. This is often due to a timeout. Please check your connection and try again.")
logger.error(
"IMAP login failed due to SSL error. This is often due to a timeout. "
"Please check your connection and try again."
)
server.logout()
sys.exit()

Expand All @@ -171,7 +185,10 @@ def imap_sync(q, logger, server):
else:
logger.warn("Message %s was not successfully processed, and will be left on IMAP server" % num)
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)
logger.error(
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
q.email_box_imap_folder
)

server.expunge()
server.close()
Expand Down Expand Up @@ -243,7 +260,7 @@ def process_queue(q, logger):

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))]
mail = [join(mail_dir, f) for f in os.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))
Expand All @@ -253,15 +270,15 @@ def process_queue(q, logger):
full_message = encoding.force_text(f.read(), errors='replace')
ticket = object_from_message(message=full_message, queue=q, logger=logger)
if ticket:
logger.info("Successfully processed message %d, ticket/comment created." % i)
logger.info("Successfully processed message %d, ticket/comment created.", i)
try:
unlink(m) # delete message file if ticket was successful
except OSError:
logger.error("Unable to delete message %d." % i)
os.unlink(m) # delete message file if ticket was successful
except OSError as e:
logger.error("Unable to delete message %d (%s).", i, str(e))
else:
logger.info("Successfully deleted message %d." % i)
logger.info("Successfully deleted message %d.", i)
else:
logger.warn("Message %d was not successfully processed, and will be left in local directory" % i)
logger.warn("Message %d was not successfully processed, and will be left in local directory", i)


def decodeUnknown(charset, string):
Expand All @@ -277,7 +294,11 @@ def decodeUnknown(charset, string):

def decode_mail_headers(string):
decoded = email.header.decode_header(string)
return u' '.join([str(msg, encoding=charset, errors='replace') if charset else str(msg) for msg, charset in decoded])
return u' '.join([
str(msg, encoding=charset, errors='replace') if charset else str(msg)
for msg, charset
in decoded
])


def create_ticket_cc(ticket, cc_list):
Expand Down Expand Up @@ -305,7 +326,7 @@ def create_ticket_cc(ticket, cc_list):
try:
ticket_cc = subscribe_to_ticket_updates(ticket=ticket, user=user, email=cced_email)
new_ticket_ccs.append(ticket_cc)
except ValidationError as err:
except ValidationError:
pass

return new_ticket_ccs
Expand Down Expand Up @@ -362,7 +383,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
logger.debug("Created new ticket %s-%s" % (ticket.queue.slug, ticket.id))

new = True
update = ''

# Old issue being re-opened
elif ticket.status == Ticket.CLOSED_STATUS:
Expand All @@ -389,7 +409,10 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)

attached = process_attachments(f, files)
for att_file in attached:
logger.info("Attachment '%s' (with size %s) successfully added to ticket from email." % (att_file[0], att_file[1].size))
logger.info(
"Attachment '%s' (with size %s) successfully added to ticket from email.",
att_file[0], att_file[1].size
)

context = safe_template_context(ticket)

Expand All @@ -402,8 +425,8 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)

ticket_cc_list = TicketCC.objects.filter(ticket=ticket).all().values_list('email', flat=True)

for email in ticket_cc_list:
notifications_to_be_sent.append(email)
for email_address in ticket_cc_list:
notifications_to_be_sent.append(email_address)

# send mail to appropriate people now depending on what objects
# were created and who was CC'd
Expand Down Expand Up @@ -453,8 +476,6 @@ def object_from_message(message, queue, logger):
# correctly. Not ideal, but this seems to work for now.
sender_email = email.utils.getaddresses(['\"' + sender.replace('<', '\" <')])[0][1]

body_plain, body_html = '', ''

cc = message.get_all('cc', None)
if cc:
# first, fixup the encoding if necessary
Expand Down Expand Up @@ -530,18 +551,23 @@ def object_from_message(message, queue, logger):
if not name:
ext = mimetypes.guess_extension(part.get_content_type())
name = "part-%i%s" % (counter, ext)

# FIXME: this code gets the paylods, then does something with it and then completely ignores it
# writing the part.get_payload(decode=True) instead; and then the payload variable is
# replaced by some dict later.
# the `payloadToWrite` has been also ignored so was commented
payload = part.get_payload()
if isinstance(payload, list):
payload = payload.pop().as_string()
payloadToWrite = payload
# payloadToWrite = payload
# check version of python to ensure use of only the correct error type
non_b64_err = TypeError
try:
logger.debug("Try to base64 decode the attachment payload")
payloadToWrite = base64.decodebytes(payload)
# payloadToWrite = base64.decodebytes(payload)
except non_b64_err:
logger.debug("Payload was not base64 encoded, using raw bytes")
payloadToWrite = payload
# payloadToWrite = payload
files.append(SimpleUploadedFile(name, part.get_payload(decode=True), mimetypes.guess_type(name)[0]))
logger.debug("Found MIME attachment %s" % name)

Expand Down
2 changes: 1 addition & 1 deletion helpdesk/locale/cs/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ msgstr "Složka pro log soubory"
#: third_party/django-helpdesk/helpdesk/models.py:306
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""

#: third_party/django-helpdesk/helpdesk/models.py:317
Expand Down
5 changes: 2 additions & 3 deletions helpdesk/locale/fr/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,10 @@ msgstr "Dossier de logs"
#: .\models.py:308
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
"Si les logs sont activés, quel dossier doit être utilisé pour stocker les "
"fichiers de logs pour cette file ? Si aucun dossier n'est défini, cela sera /"
"var/log/helpdesk/ par défaut"
"fichiers de logs pour cette file?"

#: .\models.py:319
msgid "Default owner"
Expand Down
6 changes: 4 additions & 2 deletions helpdesk/locale/ru/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,15 @@ msgstr ""

#: models.py:247
msgid "Logging Directory"
msgstr ""
msgstr "Директория логов"

#: models.py:251
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""
"Директория в которую будут сохраняться файлы с логами; стандартная конфигурация "
"используется если ничего не указано"

#: models.py:261
msgid "Default owner"
Expand Down
2 changes: 1 addition & 1 deletion helpdesk/locale/zh_CN/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ msgstr ""
#: models.py:308
msgid ""
"If logging is enabled, what directory should we use to store log files for "
"this queue? If no directory is set, default to /var/log/helpdesk/"
"this queue? The standard logging mechanims are used if no directory is set"
msgstr ""

#: models.py:319
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='queue',
name='logging_dir',
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? If no directory is set, default to /var/log/helpdesk/', max_length=200, null=True, verbose_name='Logging Directory'),
field=models.CharField(blank=True, help_text='If logging is enabled, what directory should we use to store log files for this queue? The standard logging mechanims are used if no directory is set', max_length=200, null=True, verbose_name='Logging Directory'),
),
migrations.AddField(
model_name='queue',
Expand Down
2 changes: 1 addition & 1 deletion helpdesk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class Queue(models.Model):
null=True,
help_text=_('If logging is enabled, what directory should we use to '
'store log files for this queue? '
'If no directory is set, default to /var/log/helpdesk/'),
'The standard logging mechanims are used if no directory is set'),
)

default_owner = models.ForeignKey(
Expand Down

0 comments on commit 03c6a50

Please sign in to comment.