151 lines
		
	
	
	
		
			5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			151 lines
		
	
	
	
		
			5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import re
 | 
						|
import logging
 | 
						|
 | 
						|
from email.charset import Charset as EMailCharset
 | 
						|
from django.db import transaction
 | 
						|
from django.db.models import Count
 | 
						|
from html2text import html2text
 | 
						|
from todo.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))
 | 
						|
 | 
						|
    # TODO: 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_TODO_THREAD = re.compile(r"<thread-(\d+)@django-todo>")
 | 
						|
 | 
						|
 | 
						|
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_TODO_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: %d", 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-todo) 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,
 | 
						|
            )
 | 
						|
        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},
 | 
						|
        )
 | 
						|
        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")
 |