Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
DrLuke committed Oct 2, 2018
2 parents f1160bf + 4c86505 commit 45fa276
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 32 deletions.
5 changes: 5 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
recursive-include jicket *.py
include jicket/bin/jicket
include README.md
include VERSION
include LICENSE
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Documentation Status](https://readthedocs.org/projects/jicket/badge/?version=stable)](https://jicket.readthedocs.io/en/latest/?badge=stable)
[![Documentation Status](https://readthedocs.org/projects/jicket/badge/?version=stable)](https://jicket.readthedocs.io/en/stable/?badge=stable)

# Jicket

Expand All @@ -8,6 +8,9 @@ It creates new issues for incoming mails and automatically adds email replies as
# Documentation
You can find the documentation [here](https://jicket.readthedocs.io/en/latest/).

# Github Repository
The repo for this project can be found on [Github](https://github.com/kwp-communications/jicket).

# Contributing
See [CONTRIBUTING](CONTRIBUTING.md)

Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.2.0
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '0.1.0'
with open("../VERSION", "r") as f:
release = f.read()


# -- General configuration ---------------------------------------------------
Expand Down
11 changes: 6 additions & 5 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Jira
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Configuration of jira instance on which new issues shall be created from incoming emails.

Host
URL
""""""""""""""""""""""""""""""""""
:Environment: ``JICKET_JIRA_URL``
:CLI: ``--jiraurl``
Expand All @@ -108,7 +108,7 @@ Host
:Description: URL of Jira instance that shall be used
:Example: ``jira.example.com``

Port
User
""""""""""""""""""""""""""""""""""
:Environment: ``JICKET_JIRA_USER``
:CLI: ``--jirauser``
Expand All @@ -117,7 +117,7 @@ Port
:Description: Username for Jira access
:Example: ``foo@example.com``

User
Password
""""""""""""""""""""""""""""""""""
:Environment: ``JICKET_JIRA_PASS``
:CLI: ``--jirapass``
Expand All @@ -126,7 +126,7 @@ User
:Description: Password for Jira user
:Example: ``correcthorsebatterystaple``

Password
Project
""""""""""""""""""""""""""""""""""
:Environment: ``JICKET_JIRA_PROJECT``
:CLI: ``--jiraproject``
Expand Down Expand Up @@ -168,6 +168,7 @@ Thread template
:Type: ``str``
:Required: Yes
:Description: Path to HTML file containing template for ticket thread emails. Can be absolute or relative path.
See :doc:`threadtemplate` on how to format the template.
:Example: ``/etc/jicket/threadtemplate.html``

Ticket Address
Expand Down Expand Up @@ -201,7 +202,7 @@ Loopmode

interval
Tries to run the main loop exactly every ``JICKET_LOOPTIME`` seconds. If main loop execution takes
longer than that, there is no break betwene subsequent executions.
longer than that, there is no break between subsequent executions.
:Example: ``interval``


Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ It automatically creates issues for incoming emails and appends responses as com

installation
configuration
threadtemplate


Indices and tables
Expand Down
28 changes: 24 additions & 4 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ Installation

Jicket can be installed like any other python package. Additionally a convenient docker image is provided.

Requirements
----------------------------------
Python 3.6
Jicket requires at least Python 3.6 to run.


Docker
----------------------------------
TODO
Running jicket in a docker container is a convenient way to get started quickly or for testing it locally without having
to worry about setting up the environment. You need to pass it some minimum configuration (mostly IMAP, SMTP and Jira
account data) to get it running.

`Jicket on Docker Hub <https://hub.docker.com/r/kwpcommunications/jicket/>`_

Running
^^^^^^^^^
Create a file ``env.list`` to store your environment variables. Make sure the rights for accessing the file are set
correctly, especially the global read flag (``chmod o-rwx env.list``). Configure the environment variables according to
:doc:`configuration` in a ``VAR=value`` format, e.g.:

.. code-block:: text
:caption: env.list
JICKET_IMAP_HOST=imap.example.com
JICKET_IMAP_PORT=993
JICKET_IMAP_USER=foo@example.com
JICKET_IMAP_PASS=correcthorsebatterystaple
The container is then launched:

>>> docker run -it --env-file env.list jicket



Expand Down
56 changes: 56 additions & 0 deletions docs/threadtemplate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Mail Template
====================

The contents of the confirmation mail is generated from a template. Some variables can be accessed to dynamically
generate a response to incoming emails.


Template syntax
------------------------
The template should be written as valid HTML, just as if you would write a regular mail. You can place named substitutes
for use with string interpolation in your template. The syntax for them is ``%(NAME)TYPE``. For example, if you want the
subject as a string, you'd put ``$(subject)s`` at the appropriate location in your template. See
:ref:`interpolation-vars` for a list of available variables.

An example template could look like this:

.. code-block:: html

<html>
<head></head>
<body>
<p>Hello!<br>
<br>
Thank you for contacting the support. This mail indicates that your ticket has been successfully created and will be processed soon.<br>
Please always keep the Ticket-ID in the subject, otherwise we won't be able to track your issue properly.<br>
<br>
<br>
Ticket ID: %(ticketid)s<br>
Ticket Subject: %(subject)s<br>
<br>
<br>
This mail was automatically generated by <a href="https://github.com/kwp-communications/jicket">Jicket</a>
</p>
</body>
</html>


.. _interpolation-vars:

Interpolation variables
----------------------------

Subject
""""""""""""""""""""""""""""""""""
:Name: ``subject``
:Type: ``s``
:Description: Subject of ticket
:Example: Re: The Website Is Down


Ticket ID
""""""""""""""""""""""""""""""""""
:Name: ``ticketid``
:Type: ``s``
:Description: Hashed ID of ticket
:Example: K6NPD4
2 changes: 1 addition & 1 deletion jicket/jicket/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def jicketapp():
**argparse_env("JICKET_FOLDER_INBOX", "INBOX"))
parser.add_argument("--foldersuccess", type=str,
help="Folder in which successfully imported mails are put",
**argparse_env("JICKET_FOLDER_SUCCESS", "jicket-incoming"))
**argparse_env("JICKET_FOLDER_SUCCESS", "jicket"))
parser.add_argument("--threadtemplate", type=str,
help="Folder in which successfully imported mails are put",
**argparse_env("JICKET_THREAD_TEMPLATE"))
Expand Down
48 changes: 42 additions & 6 deletions jicket/jicket/jiraintegration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Creates or updates issue from Mail"""

from typing import List, Tuple
from typing import List, Tuple, Dict
from jicket.mailhandling import ProcessedMail
import jira
import jicket.log as log
Expand All @@ -27,11 +27,48 @@ def __init__(self, mail: ProcessedMail, config: JiraConfig):

self.jira = jira.JIRA(self.config.jiraHost, basic_auth=(self.config.jiraUser, self.config.jiraPass))

self.textbodies = {} # type: Dict[str] # All text bodies found in email

def gettext(self) -> None:
"""Get all text bodies from email"""
if self.mail.parsed.is_multipart():
for part in self.mail.parsed.get_payload():
if part.get_content_maintype() == "text":
self.textbodies[part.get_content_subtype()] = part.get_payload(decode=True).decode(part.get_content_charset())
else:
self.textbodies[self.mail.parsed.get_content_subtype()] = self.mail.parsed.get_payload(
decode=True).decode(self.mail.parsed.get_content_charset())

def getattachments(self) -> None:
"""Fetch all attachments"""
pass

def textfrombodies(self) -> str:
"""Convert text bodies to text that can be attached to an issue"""
type_priority = ["plain", "html", "other"] # TODO: Make configurable

for texttype in type_priority:
if texttype == "plain" and texttype in self.textbodies:
"""Text is plain, so it can be used verbatim"""
return self.textbodies[texttype]
if texttype == "html" and texttype in self.textbodies:
"""HTML text. Convert to markup with html2text and remove extra spaces"""
text = html2text.html2text(self.textbodies[texttype])
# Remove every second newline which is added to distinguish between paragraphs in Markdown, but makes
# the jira ticket hard to read.
return re.sub("(\n.*?)\n", "\g<1>", text)
if texttype == "other" and len(self.textbodies):
# If no other text is found, return the first available body if any.
return self.textbodies[list(self.textbodies.keys())[0]]
return "The email contained no text bodies."

def processMail(self) -> Tuple[bool, bool]:
"""Updates or creates new issue from mail
:returns: Tuple[bool, bool] Tuple indicating the jira import success and if this is a new issue"""
self.gettext()
self.getattachments()

issues = self.findIssue()
try:
if issues:
Expand All @@ -44,14 +81,12 @@ def processMail(self) -> Tuple[bool, bool]:
except jira.exceptions.JIRAError:
return False, False


def findIssue(self) -> List[jira.Issue]:
"""Check if issue for ticketid exists already"""
issues = self.jira.search_issues("project = %s AND summary~'\\\\[\\\\#%s\\\\]'" % (self.config.project, self.mail.prefixedhash))

return issues


def newIssue(self):
"""Create a new issue from Mail"""
log.info("Creating new Issue for #%s in project %s" % (self.mail.prefixedhash, self.config.project))
Expand All @@ -60,7 +95,9 @@ def newIssue(self):
description = ""
description += "Imported by Jicket (SequentialID: %i)\n" % self.mail.ticketid
description += "From: %s\n\n\n" % self.mail.parsed["From"]
description += re.sub("(\n.*?)\n", "\g<1>", html2text.html2text(self.mail.body)) # Remove every second newline which is added to distinguish between paragraphs in Markdown, but makes the jira ticket hard to read.
description += self.textfrombodies()

# TODO: Attachments

issuedict = {
"project": {"key": self.config.project},
Expand All @@ -71,13 +108,12 @@ def newIssue(self):

self.jira.create_issue(fields=issuedict)


def updateIssue(self, issue: jira.Issue):
"""Update issue from mail"""
log.info("Updating Issue for #%s in project %s" % (self.mail.prefixedhash, self.config.project))

commenttext = ""
commenttext += "From: %s\n\n\n" % self.mail.parsed["From"]
commenttext += re.sub("(\n.*?)\n", "\g<1>", html2text.html2text(self.mail.body))
commenttext += self.textfrombodies()

comment = self.jira.add_comment(issue, commenttext) # TODO: error checking
13 changes: 2 additions & 11 deletions jicket/jicket/mailhandling.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,7 @@ def process(self) -> None:
"""Parse email and fetch body and all attachments"""
self.parsed = email.message_from_bytes(self.rawmailcontent) # type: email.message.EmailMessage

if self.parsed.is_multipart():
for part in self.parsed.get_payload():
if part.get_content_maintype() == "text":
# Append all text parts together. Usually there shouldn't be more than one text part.
self.body += part.get_payload(decode=True).decode(part.get_content_charset())
else:
if self.parsed.get_content_maintype() == "text":
self.body += self.parsed.get_payload()

self.subject = self.parsed["subject"]
# TODO: Get all attachments

if self.parsed["X-Jicket-Initial-ReplyID"] is not None and self.parsed["X-Jicket-Initial-ReplyID"] == self.parsed["In-Reply-To"]:
self.threadstarter = True
Expand Down Expand Up @@ -195,7 +185,7 @@ def quit(self):
self.SMTP.quit()

def sendmail(self, mail: email.message.Message):
self.SMTP.sendmail(mail["From"], mail["To"].split(","), mail.as_string())
self.SMTP.sendmail(mail["From"], mail["To"].split(",") + mail["CC"].split(","), mail.as_string())

def sendTicketStart(self, mail: ProcessedMail):
"""Sends the initial mail to start an email thread from an incoming email"""
Expand All @@ -217,6 +207,7 @@ def sendTicketStart(self, mail: ProcessedMail):

# Set other headers
threadstarter["To"] = "%s, %s" % (mail.parsed["From"], self.mailconfig.ticketAddress)
threadstarter["CC"] = mail.parsed["CC"]
threadstarter["From"] = self.mailconfig.ticketAddress
threadstarter["In-Reply-To"] = mail.parsed["Message-ID"].rstrip()
threadstarter["Subject"] = "[#%s%s] %s" % (self.mailconfig.idPrefix, mail.tickethash, mail.subject)
Expand Down
8 changes: 8 additions & 0 deletions release/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.7

COPY threadtemplate.html /etc/jicket/threadtemplate.html
ENV JICKET_THREAD_TEMPLATE=/etc/jicket/threadtemplate.html

RUN pip install jicket

ENTRYPOINT /usr/local/bin/jicket
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<br>
Ticket ID: %(ticketid)s<br>
Ticket Subject: %(subject)s<br>
<br>
<br>
This mail was automatically generated by <a href="https://github.com/kwp-communications/jicket">Jicket</a>
</p>
</body>
</html>

0 comments on commit 45fa276

Please sign in to comment.