From 923b481ce94694487a98dcf430b2d8f5a6e288ff Mon Sep 17 00:00:00 2001 From: Gael Le Mignot Date: Thu, 20 Jun 2024 15:20:45 +0200 Subject: [PATCH] Initial shot of POP email answer --- .../commands/teleforma-get-postman-replies.py | 149 ++++++++++++++++++ .../templates/postman/email_user_subject.txt | 2 +- teleforma/templatetags/teleforma_tags.py | 6 + teleforma/utils.py | 13 +- 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 teleforma/management/commands/teleforma-get-postman-replies.py diff --git a/teleforma/management/commands/teleforma-get-postman-replies.py b/teleforma/management/commands/teleforma-get-postman-replies.py new file mode 100644 index 00000000..f8588272 --- /dev/null +++ b/teleforma/management/commands/teleforma-get-postman-replies.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +from django.conf import settings +from django.core.management.base import BaseCommand +from postman.models import * +import re +import logging +import datetime +import time +from django.contrib.sites.models import Site +from django.core.exceptions import ObjectDoesNotExist +import email + +from teleforma import utils + +import poplib + +def decode_header(header, charset): + """ + Decode a header field + """ + header = email.header.decode_header(header) + res = [] + for value, encoding in header: + res.append(value) + return " ".join(res) + +def smart_walk(msg, in_alternative = False): + """ + Like the walk method, but ignores html in multipart/alternative + """ + ctype = msg.get_content_type() + if msg.is_multipart(): + if ctype == "multipart/alternative": + in_alternative = True + yield msg + subparts = msg.get_payload() + for subpart in subparts: + for subsubpart in smart_walk(subpart, in_alternative): + yield subsubpart + elif not in_alternative or not "text/html" in ctype: + yield msg + +class Logger: + """A logging object""" + + def __init__(self, file): + self.logger = logging.getLogger('teleforma') + self.hdlr = logging.FileHandler(file) + self.formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + self.hdlr.setFormatter(self.formatter) + self.logger.addHandler(self.hdlr) + self.logger.setLevel(logging.INFO) + + +class Command(BaseCommand): + help = "Fetch mails from a POP account to feed postman" + language_code = 'fr_FR' + + def add_arguments(self, parser): + parser.add_argument('log_file') + + def process(self, message): + mail = email.message_from_bytes(message) + title = mail.get("subject", "").strip() + title = decode_header(title, 'utf-8') + + parts = title.split('[') + if len(parts) == 1: + self.logger.warning("Invalid title (no start delimiter): {%s}" % title) + return + subject = '['.join(parts[:-1]) + part = parts[-1] + if not part.endswith("]"): + self.logger.warning("Invalid title (no start delimiter): {%s}" % title) + return + msghash = part[:-1] + msgid = msghash[:-4] + msgid = int(msgid, 16) + if utils.generate_hash(msgid) != msghash: + self.logger.warning("Invalid hash {%s} for {%s}" % (msghash, msgid)) + return + msg = Message.objects.get(pk = msgid) + + self.logger.info("Found reply to message %s" % msgid) + + body = "" + for part in smart_walk(mail): + payload = part.get_payload(decode = True) + if not isinstance(payload, bytes): + # ignore multipart here + continue + filename = part.get_filename() + content_type = part.get_content_type() + if not filename and content_type.startswith("text/"): + body += payload.decode(part.get_content_charset() or "ascii") + + # Purge quotes and signatures from body + lines = [] + for line in body.split("\n"): + stripped = line.strip() + if stripped.startswith(">"): + continue + if stripped.startswith("----"): + break + if stripped.startswith("--") and not line[2:].strip(): + break + lines.append(stripped) + + while lines and not lines[0]: + del lines[0] + while lines and not lines[-1]: + del lines[-1] + body = '\n'.join(lines) + + mess = Message(sender=msg.recipient, recipient=msg.sender, + subject=subject, body=body) + mess.moderation_status = 'a' + mess.save() + site = Site.objects.all()[0] + notify_user(mess, 'acceptance', site) + + def handle(self, *args, **options): + log_file = options['log_file'] + logger = Logger(log_file) + logger.logger.info('########### Processing #############') + self.logger = logger.logger + + pop = poplib.POP3_SSL(settings.REPLY_POP_SERVER) + pop.user(settings.REPLY_POP_LOGIN) + pop.pass_(settings.REPLY_POP_PASSWORD) + _, messages, _ = pop.list() + self.logger.info("%d messages available" % len(messages)) + try: + for msg in messages: + msgid, _ = msg.split() + msgid = msgid.decode('ascii') + _, msg, _ = pop.retr(msgid) + msg = b"\r\n".join(msg) + try: + self.process(msg) + pop.dele(msgid) + except Exception as e: + self.logger.warning("Error in message %s: %s" % (msgid, str(e))) + raise + finally: + pop.quit() + + self.logger.info('############## Done #################') diff --git a/teleforma/templates/postman/email_user_subject.txt b/teleforma/templates/postman/email_user_subject.txt index cace60c0..6c72d09c 100644 --- a/teleforma/templates/postman/email_user_subject.txt +++ b/teleforma/templates/postman/email_user_subject.txt @@ -1 +1 @@ -{% load i18n %}{% autoescape off %}{% blocktrans with object.subject as subject and site.name as sitename %}Message "{{ subject }}" on the site {{ sitename }}{% endblocktrans %}{% endautoescape %} \ No newline at end of file +{% load teleforma_tags %}{% load i18n %}{% autoescape off %}{% blocktrans with object.subject as subject and site.name as sitename %}Message "{{ subject }}" on the site {{ sitename }}{% endblocktrans %} [{% generate_msg_id object %}]{% endautoescape %} diff --git a/teleforma/templatetags/teleforma_tags.py b/teleforma/templatetags/teleforma_tags.py index 73930a2f..f34f1d64 100644 --- a/teleforma/templatetags/teleforma_tags.py +++ b/teleforma/templatetags/teleforma_tags.py @@ -55,6 +55,7 @@ from ..exam.models import Quota, Script from ..models.core import Document, Professor from ..models.crfpa import IEJ, Course, NewsItem, Training from ..views import get_courses +from ..utils import generate_hash from collections import defaultdict @@ -495,6 +496,11 @@ def conference_publication(context, conference): period = context['period'] return conference.publication_info(period) +@register.simple_tag(takes_context=True) +def generate_msg_id(context, message): + mid = message.id + return generate_hash(mid) + # @register.simple_tag(takes_context=True) # def course_media(context): diff --git a/teleforma/utils.py b/teleforma/utils.py index 6a21d79e..c26835d8 100644 --- a/teleforma/utils.py +++ b/teleforma/utils.py @@ -1,5 +1,16 @@ import mimetypes +import hashlib +from django.conf import settings def guess_mimetypes(url): url = url.split("?")[0] - return mimetypes.guess_type(url)[0] \ No newline at end of file + return mimetypes.guess_type(url)[0] + +def generate_hash(mid): + """ + Generate a hash for messages + """ + mid = "%x" % mid + msg = '%s-%s' % (settings.SECRET_KEY, mid) + md5 = hashlib.md5(msg.encode('ascii')).hexdigest() + return '%s%s' % (mid, md5[:4]) -- 2.39.5