diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..e325b33f
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,44 @@
+ARG PYTHON_VERSION=3.11.5-slim-bookworm
+
+# define an alias for the specfic python version used in this file.
+FROM python:${PYTHON_VERSION} as python
+
+FROM python as python-build-stage
+
+# Install apt packages
+RUN apt-get update && apt-get install --no-install-recommends -y \
+ # dependencies for building Python packages
+ build-essential
+
+# Requirements are installed here to ensure they will be cached.
+COPY ./requirements.txt ./requirements-dev.txt /
+
+# Create Python Dependency and Sub-Dependency Wheels
+RUN pip wheel --wheel-dir /usr/src/app/wheels \
+ -r requirements.txt \
+ -r requirements-dev.txt
+
+FROM python as python-run-stage
+
+ARG APP_HOME=/app
+
+ENV PYTHONUNBUFFERED 1
+ENV PYTHONDONTWRITEBYTECODE 1
+
+WORKDIR ${APP_HOME}
+
+COPY --from=python-build-stage /usr/src/app/wheels /wheels/
+
+# use wheels to install python dependencies
+RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \
+ && rm -rf /wheels/
+
+COPY ./entrypoint /entrypoint
+RUN sed -i 's/\r$//g' /entrypoint && chmod +x /entrypoint
+
+FROM python-run-stage AS backend
+
+# copy application code to WORKDIR
+COPY . ${APP_HOME}
+
+ENTRYPOINT ["/entrypoint"]
diff --git a/README.rst b/README.rst
index 5eae9856..49bcab6e 100644
--- a/README.rst
+++ b/README.rst
@@ -27,11 +27,17 @@ get started with testing or developing `django-helpdesk`. The demo project
resides in the `demo/` top-level folder.
It's likely that you can start up a demo project server by running
-only the command::
+only the command:
make rundemo
-then pointing your web browser at `localhost:8080`.
+or with docker:
+
+ docker build . -t demodesk
+ docker run --rm -v "$PWD:/app" -p 8080:8080 demodesk
+
+then pointing your web browser at http://localhost:8080 (log in as user
+`admin`` with password `Test1234`).
For more information and options, please read the `demo/README.rst` file.
@@ -51,7 +57,7 @@ Installation
`django-helpdesk` requires:
* Python 3.8+
-* Django 3.2 LTS highly recommended (early adopters may test Django 4)
+* Django 3.2 LTS or Django 4.*
You can quickly install the latest stable version of `django-helpdesk`
app via `pip`::
@@ -72,6 +78,9 @@ Developer Environment
---------------------
Follow these steps to set up your development environment to contribute to helpdesk:
+ - check out the helpdesk app to your local file system::
+ git clone https://github.com/django-helpdesk/django-helpdesk.git
+
- install a virtual environment
- using virtualenv from the helpdesk base folder do::
virtualenv .venv && source .venv/bin/activate
@@ -79,6 +88,12 @@ Follow these steps to set up your development environment to contribute to helpd
- install the requirements for development::
pip install -r requirements.txt -r requirements-dev.txt
+ - install the requirements for testing as well::
+ pip install -r requirements.txt -r requirements-dev.txt -r requirements-testing.txt
+
+To reactivate a VENV just run:
+ source .venv/bin/activate
+
To see option for the Makefile run: `make`
The project enforces a standardized formatting in the CI/CD pipeline. To ensure you have the correct formatting run::
diff --git a/docs/settings.rst b/docs/settings.rst
index d8f5f7af..874afa34 100644
--- a/docs/settings.rst
+++ b/docs/settings.rst
@@ -37,7 +37,6 @@ If you want to override the default settings for your users, create ``HELPDESK_D
'tickets_per_page': 25
}
-
Generic Options
---------------
These changes are visible throughout django-helpdesk
@@ -86,6 +85,10 @@ These changes are visible throughout django-helpdesk
**Default:** ``HELPDESK_MAX_EMAIL_ATTACHMENT_SIZE = 512000``
+- **VALID_EXTENSIONS** Valid extensions for file types that can be attached to tickets
+
+ **Default:** ``VALID_EXTENSIONS = ['.txt', '.asc', '.htm', '.html', '.pdf', '.doc', '.docx', '.odt', '.jpg', '.png', '.eml']
+
- **QUEUE_EMAIL_BOX_UPDATE_ONLY** Only process mail with a valid tracking ID; all other mail will be ignored instead of creating a new ticket.
**Default:** ``QUEUE_EMAIL_BOX_UPDATE_ONLY = False``
diff --git a/entrypoint b/entrypoint
new file mode 100644
index 00000000..922eca95
--- /dev/null
+++ b/entrypoint
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+set -o errexit
+set -o pipefail
+set -o nounset
+
+pip3 install -e . --user || pip3 install -e .
+pip3 install -e demo --user || pip3 install -e demo
+python3 demo/manage.py migrate --noinput
+DJANGO_SUPERUSER_PASSWORD=Test1234 python3 demo/manage.py createsuperuser --username admin --email helpdesk@example.com --noinput
+# Install fixtures
+python3 demo/manage.py loaddata emailtemplate.json
+python3 demo/manage.py loaddata demo.json
+python3 demo/manage.py runserver 0:8080
diff --git a/helpdesk/email.py b/helpdesk/email.py
index 68aa06b6..08e14f96 100644
--- a/helpdesk/email.py
+++ b/helpdesk/email.py
@@ -6,8 +6,6 @@ See LICENSE for details.
"""
# import base64
-
-
from bs4 import BeautifulSoup
from datetime import timedelta
from django.conf import settings as django_settings
@@ -18,7 +16,8 @@ from django.db.models import Q
from django.utils import encoding, timezone
from django.utils.translation import gettext as _
import email
-from email.message import Message
+from email import policy
+from email.message import EmailMessage, MIMEPart
from email.utils import getaddresses
from email_reply_parser import EmailReplyParser
from helpdesk import settings
@@ -39,7 +38,7 @@ import ssl
import sys
from time import ctime
import typing
-from typing import List, Tuple
+from typing import List
# import User model, which may be a custom model
@@ -53,6 +52,9 @@ STRIPPED_SUBJECT_STRINGS = [
"Automatic reply: ",
]
+# Allow a custom default attached email name for the HTML formatted email if one is found
+HTML_EMAIL_ATTACHMENT_FILENAME = _("email_html_body.html")
+
def process_email(quiet=False):
for q in Queue.objects.filter(
@@ -75,7 +77,6 @@ def process_email(quiet=False):
logger.propagate = False
if quiet:
logger.propagate = False # do not propagate to root logger that would log to console
-
# 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(
@@ -141,7 +142,7 @@ def pop3_sync(q, logger, server):
full_message = encoding.force_str(
"\n".join(raw_content), errors='replace')
try:
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ ticket = extract_email_metadata(message=full_message, queue=q, logger=logger)
except IgnoreTicketException:
logger.warn(
"Message %s was ignored and will be left on POP3 server" % msgNum)
@@ -198,7 +199,7 @@ def imap_sync(q, logger, server):
data = server.fetch(num, '(RFC822)')[1]
full_message = encoding.force_str(data[0][1], errors='replace')
try:
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ ticket = extract_email_metadata(message=full_message, queue=q, logger=logger)
except IgnoreTicketException:
logger.warn("Message %s was ignored and will be left on IMAP server" % num)
except DeleteIgnoredTicketException:
@@ -252,13 +253,11 @@ def imap_oauth_sync(q, logger, server):
)
server.debug = settings.HELPDESK_IMAP_DEBUG_LEVEL
-
# TODO: Perhaps store the authentication string template externally? Settings? Queue Table?
server.authenticate(
"XOAUTH2",
lambda x: f"user={q.email_box_user}\x01auth=Bearer {token['access_token']}\x01\x01".encode(),
)
-
# Select the Inbound Mailbox folder
server.select(q.email_box_imap_folder)
@@ -285,7 +284,7 @@ def imap_oauth_sync(q, logger, server):
full_message = encoding.force_str(data[0][1], errors='replace')
try:
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ ticket = extract_email_metadata(message=full_message, queue=q, logger=logger)
except IgnoreTicketException as itex:
logger.warn(f"Message {num} was ignored. {itex}")
@@ -312,7 +311,6 @@ def imap_oauth_sync(q, logger, server):
"IMAP retrieve failed. Is the folder '%s' spelled correctly, and does it exist on the server?",
q.email_box_imap_folder
)
-
# Purged Flagged Messages & Logout
server.expunge()
server.close()
@@ -405,7 +403,7 @@ def process_queue(q, logger):
with open(m, 'r') as f:
full_message = encoding.force_str(f.read(), errors='replace')
try:
- ticket = object_from_message(message=full_message, queue=q, logger=logger)
+ ticket = extract_email_metadata(message=full_message, queue=q, logger=logger)
except IgnoreTicketException:
logger.warn("Message %d was ignored and will be left in local directory", i)
except DeleteIgnoredTicketException:
@@ -429,7 +427,7 @@ def process_queue(q, logger):
def decodeUnknown(charset, string):
- if type(string) is not str:
+ if string and not isinstance(string, str):
if not charset:
try:
return str(string, encoding='utf-8', errors='replace')
@@ -468,11 +466,10 @@ def is_autoreply(message):
def create_ticket_cc(ticket, cc_list):
if not cc_list:
return []
-
# Local import to deal with non-defined / circular reference problem
- from helpdesk.views.staff import subscribe_to_ticket_updates, User
new_ticket_ccs = []
+ from helpdesk.views.staff import subscribe_to_ticket_updates, User
for __, cced_email in cc_list:
cced_email = cced_email.strip()
@@ -538,7 +535,6 @@ def create_object_from_email_message(message, ticket_id, payload, files, logger)
ticket.merged_to.ticket)
# Use the ticket in which it was merged to for next operations
ticket = ticket.merged_to
-
# New issue, create a new
" - - payload = ( - '' - '
' - '' - '' - '%s' - '