123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- import re
- import logging
- from email.charset import Charset as EMailCharset
- from django.db import transaction
- from django.db.models import Count
- from django.contrib.auth import get_user_model
- from django.conf import settings
- from html2text import html2text
- from email.utils import parseaddr
- from tickets.models import Comment, Task, TaskList
- logger = logging.getLogger(__name__)
- def part_decode(message):
- charset = ("ascii", "ignore")
- email_charset = message.get_content_charset()
- if email_charset:
- charset = (EMailCharset(email_charset).input_charset,)
- body = message.get_payload(decode=True)
- return body.decode(*charset)
- def message_find_mime(message, mime_type):
- for submessage in message.walk():
- if submessage.get_content_type() == mime_type:
- return submessage
- return None
- def message_text(message):
- text_part = message_find_mime(message, "text/plain")
- if text_part is not None:
- return part_decode(text_part)
- html_part = message_find_mime(message, "text/html")
- if html_part is not None:
- return html2text(part_decode(html_part))
- # tickets: find something smart to do when no text if found
- return ""
- def format_task_title(format_string, message):
- return format_string.format(subject=message["subject"], author=message["from"])
- DJANGO_TICKETS_THREAD = re.compile(r"<thread-(\d+)@django-tickets>")
- def parse_references(task_list, references):
- related_messages = []
- answer_thread = None
- for related_message in references.split():
- logger.info("checking reference: %r", related_message)
- match = re.match(DJANGO_TICKETS_THREAD, related_message)
- if match is None:
- related_messages.append(related_message)
- continue
- thread_id = int(match.group(1))
- new_answer_thread = Task.objects.filter(task_list=task_list, pk=thread_id).first()
- if new_answer_thread is not None:
- answer_thread = new_answer_thread
- if answer_thread is None:
- logger.info("no answer thread found in references")
- else:
- logger.info("found an answer thread: %s", str(answer_thread))
- return related_messages, answer_thread
- def insert_message(task_list, message, priority, task_title_format):
- if "message-id" not in message:
- logger.warning("missing message id, ignoring message")
- return
- if "from" not in message:
- logger.warning('missing "From" header, ignoring message')
- return
- if "subject" not in message:
- logger.warning('missing "Subject" header, ignoring message')
- return
- logger.info(
- "received message:\t"
- f"[Subject: {message['subject']}]\t"
- f"[Message-ID: {message['message-id']}]\t"
- f"[References: {message['references']}]\t"
- f"[To: {message['to']}]\t"
- f"[From: {message['from']}]"
- )
- # Due to limitations in MySQL wrt unique_together and TextField (grrr),
- # we must use a CharField rather than TextField for message_id.
- # In the unlikeley event that we get a VERY long inbound
- # message_id, truncate it to the max_length of a MySQL CharField.
- original_message_id = message["message-id"]
- message_id = (
- (original_message_id[:252] + "...")
- if len(original_message_id) > 255
- else original_message_id
- )
- message_from = message["from"]
- text = message_text(message)
- related_messages, answer_thread = parse_references(task_list, message.get("references", ""))
- # find the most relevant task to add a comment on.
- # among tasks in the selected task list, find the task having the
- # most email comments the current message references
- best_task = (
- Task.objects.filter(task_list=task_list, comment__email_message_id__in=related_messages)
- .annotate(num_comments=Count("comment"))
- .order_by("-num_comments")
- .only("id")
- .first()
- )
- # if no related comment is found but a thread message-id
- # (generated by django-tickets) could be found, use it
- if best_task is None and answer_thread is not None:
- best_task = answer_thread
- with transaction.atomic():
- if best_task is None:
- best_task = Task.objects.create(
- priority=priority,
- title=format_task_title(task_title_format, message),
- task_list=task_list,
- created_by=match_user(message_from),
- )
- logger.info("using task: %r", best_task)
- comment, comment_created = Comment.objects.get_or_create(
- task=best_task,
- email_message_id=message_id,
- defaults={"email_from": message_from, "body": text},
- author=match_user(message_from), # tickets: Write test for this
- )
- logger.info("created comment: %r", comment)
- def tracker_consumer(
- producer, group=None, task_list_slug=None, priority=1, task_title_format="[MAIL] {subject}"
- ):
- task_list = TaskList.objects.get(group__name=group, slug=task_list_slug)
- for message in producer:
- try:
- insert_message(task_list, message, priority, task_title_format)
- except Exception:
- # ignore exceptions during insertion, in order to avoid
- logger.exception("got exception while inserting message")
- def match_user(email):
- """ This function takes an email and checks for a registered user."""
- if not settings.TICKETS_MAIL_USER_MAPPER:
- user = None
- else:
- try:
- # Find the first user that matches the email
- user = get_user_model().objects.get(email=parseaddr(email)[1])
- except get_user_model().DoesNotExist:
- user = None
- return user
|