]> git.sommitrealweird.co.uk Git - eoc.git/blobdiff - eoc.py
add feature: configurable subject prefixing for mailing lists
[eoc.git] / eoc.py
diff --git a/eoc.py b/eoc.py
index c7fb0c169b8a78a6cc036074ebb8ec8560176f6f..e328a4a8b58063454f33cb0dca128d9fd2852e3d 100644 (file)
--- a/eoc.py
+++ b/eoc.py
@@ -4,11 +4,11 @@ This is a simple mailing list manager that mimicks the ezmlm-idx mail
 address commands. See manual page for more information.
 """
 
-VERSION = "1.2.1"
+VERSION = "1.2.6"
 PLUGIN_INTERFACE_VERSION = "1"
 
 import getopt
-import md5
+import hashlib
 import os
 import shutil
 import smtplib
@@ -78,7 +78,34 @@ COMMANDS = SIMPLE_COMMANDS + SUB_COMMANDS + HASH_COMMANDS
 
 
 def md5sum_as_hex(s):
-    return md5.new(s).hexdigest()
+    return hashlib.md5(s).hexdigest()
+
+def forkexec(argv, text):
+    """Run a command (given as argv array) and write text to its stdin"""
+    (r, w) = os.pipe()
+    pid = os.fork()
+    if pid == -1:
+        raise Exception("fork failed")
+    elif pid == 0:
+        os.dup2(r, 0)
+        os.close(r)
+        os.close(w)
+        fd = os.open("/dev/null", os.O_RDWR)
+        os.dup2(fd, 1)
+        os.dup2(fd, 2)
+        os.execvp(argv[0], argv)
+        sys.exit(1)
+    else:
+        os.close(r)
+        os.write(w, text)
+        os.close(w)
+        (pid2, exit) = os.waitpid(pid, 0)
+        if pid != pid2:
+            raise Exception("os.waitpid for %d returned for %d" % (pid, pid2))
+        if exit != 0:
+            raise Exception("subprocess failed, exit=0x%x" % exit)
+        return exit
+
 
 environ = None
 
@@ -395,22 +422,28 @@ class MailingListManager:
                "\n    ".join(text[:text.find("\n\n")].split("\n"))))
         if recipients:
             if self.smtp_server:
-                smtp = smtplib.SMTP(self.smtp_server)
-                smtp.sendmail(envelope_sender, recipients, text)
-                smtp.quit()
+                try:
+                    smtp = smtplib.SMTP(self.smtp_server)
+                    smtp.sendmail(envelope_sender, recipients, text)
+                    smtp.quit()
+                except:
+                    error("Error sending SMTP mail, mail probably not sent")
+                    sys.exit(1)
             elif self.qmqp_server:
-                q = qmqp.QMQP(self.qmqp_server)
-                q.sendmail(envelope_sender, recipients, text)
-                q.quit()
+                try:
+                    q = qmqp.QMQP(self.qmqp_server)
+                    q.sendmail(envelope_sender, recipients, text)
+                    q.quit()
+                except:
+                    error("Error sending QMQP mail, mail probably not sent")
+                    sys.exit(1)
             else:
-                recipients = string.join(recipients, " ")
-                f = os.popen("%s -oi -f '%s' %s" % 
-                                 (self.sendmail, 
-                                  envelope_sender, 
-                                  recipients),
-                             "w")
-                f.write(text)
-                f.close()
+                status = forkexec([self.sendmail, "-oi", "-f", 
+                                   envelope_sender] + recipients, text)
+                if status:
+                    error("%s returned %s, mail sending probably failed" %
+                           (self.sendmail, status))
+                    sys.exit((status >> 8) & 0xff)
         else:
             debug("send_mail: no recipients, not sending")
 
@@ -436,6 +469,7 @@ class MailingList:
         self.cp.set("list", "ignore-bounce", "no")
         self.cp.set("list", "language", "")
         self.cp.set("list", "pristine-headers", "")
+        self.cp.set("list", "subject-prefix", "")
 
         self.dirname = os.path.join(self.mlm.dotdir, name)
         self.make_listdir()
@@ -463,6 +497,8 @@ class MailingList:
 
     def read_stdin(self):
         data = sys.stdin.read()
+        # Convert CRLF to plain LF
+        data = "\n".join(data.split("\r\n"))
         # Skip Unix mbox "From " mail start indicator
         if data[:5] == "From ":
             data = string.split(data, "\n", 1)[1]
@@ -491,28 +527,32 @@ class MailingList:
         return True
     
     def mime_encode_headers(self, text):
-        headers, body = text.split("\n\n", 1)
-    
-        list = []
-        for line in headers.split("\n"):
-            if line[0].isspace():
-                list[-1] += line
-            else:
-                list.append(line)
-    
-        headers = []
-        for header in list:
-            if self.nice_7bit(header):
-                headers.append(header)
-            else:
-                if ": " in header:
-                    name, content = header.split(": ", 1)
+        try:
+            headers, body = text.split("\n\n", 1)
+        
+            list = []
+            for line in headers.split("\n"):
+                if line[0].isspace():
+                    list[-1] += line
                 else:
-                    name, content = header.split(":", 1)
-                hdr = email.Header.Header(content, "utf-8")
-                headers.append(name + ": " + hdr.encode())
-    
-        return "\n".join(headers) + "\n\n" + body
+                    list.append(line)
+        
+            headers = []
+            for header in list:
+                if self.nice_7bit(header):
+                    headers.append(header)
+                else:
+                    if ": " in header:
+                        name, content = header.split(": ", 1)
+                    else:
+                        name, content = header.split(":", 1)
+                    hdr = email.Header.Header(content, "utf-8")
+                    headers.append(name + ": " + hdr.encode())
+        
+            return "\n".join(headers) + "\n\n" + body
+        except:
+            info("Cannot MIME encode header, using original ones, sorry")
+            return text
 
     def template(self, template_name, dict):
         lang = self.cp.get("list", "language")
@@ -548,6 +588,7 @@ class MailingList:
             return
         if self.cp.get("list", "pristine-headers") != "yes":
             text = self.mime_encode_headers(text)
+        text = self.add_subject_prefix(text)
         self.mlm.send_mail(envelope_sender, recipients, text)
 
     def send_info_message(self, recipients, template_name, dict):
@@ -617,7 +658,7 @@ class MailingList:
                                           "boundary": self.invent_boundary(),
                                        })
         else:
-            self.info_message([recipient], "setlist-sorry", {})
+            self.send_info_message([recipient], "setlist-sorry", {})
 
     def parse_setlist_addresses(self, text):
         body = text.split("\n\n", 1)[1]
@@ -691,6 +732,7 @@ class MailingList:
     def obey_owner(self, text):
         sender = get_from_environ("SENDER")
         recipients = self.cp.get("list", "owners").split()
+        text = self.add_subject_prefix(text)
         self.mlm.send_mail(sender, recipients, text)
 
     def obey_subscribe_or_unsubscribe(self, dict, template_name, command, 
@@ -838,7 +880,7 @@ class MailingList:
         else:
             return ""
 
-    def remove_some_headers(self, mail, headers_to_remove):
+    def headers_and_body(self, mail):
         endpos = mail.find("\n\n")
         if endpos == -1:
             endpos = mail.find("\n\r\n")
@@ -846,15 +888,27 @@ class MailingList:
                 return mail
         headers = mail[:endpos].split("\n")
         body = mail[endpos:]
+        return (headers, body)
+
+    def remove_some_headers(self, mail, headers_to_remove):
+        headers, body = self.headers_and_body(mail)
+
+        headers_to_remove = [x.lower() for x in headers_to_remove]
     
         remaining = []
         add_continuation_lines = 0
+
         for header in headers:
-            pos = header.find(":")
-            if pos == -1:
+            if header[0] in [' ','\t']:
+                # this is a continuation line
                 if add_continuation_lines:
                     remaining.append(header)
             else:
+                pos = header.find(":")
+                if pos == -1:
+                    # malformed message, try to remove the junk
+                    add_continuation_lines = 0
+                    continue
                 name = header[:pos].lower()
                 if name in headers_to_remove:
                     add_continuation_lines = 0
@@ -880,8 +934,13 @@ class MailingList:
         return text + self.template("footer", {})
 
     def send_mail_to_subscribers(self, text):
+        text = self.remove_some_headers(text, ["list-id", "list-help",
+                                               "list-unsubscribe",
+                                               "list-subscribe", "list-post",
+                                               "list-owner", "precedence"])
         text = self.headers_to_add() + self.list_headers() + \
                self.headers_to_remove(text)
+        text = self.add_subject_prefix(text)
         text = self.append_footer(text)
         text, = self.mlm.call_plugins("send_mail_to_subscribers_hook",
                                      self, text)
@@ -894,6 +953,33 @@ class MailingList:
             addresses = self.subscribers.in_group(group)
             self.mlm.send_mail(bounce, addresses, text)
 
+    def add_subject_prefix(self, text):
+        """Given a full-text mail, munge its subject header with the configured
+           subject prefix (if any) and return the updated mail text.
+        """
+        headers, body = self.headers_and_body(text)
+
+        prefix = self.cp.get("list", "subject-prefix")
+
+        # We don't need to do anything special to deal with multi-line
+        # subjects since adding the prefix to the first line of the subject
+        # and leaving the later lines untouched is sufficient.
+        if prefix:
+            has_subject = False
+            for header in headers:
+                if header.startswith('Subject:'):
+                    has_subject = True
+                    if prefix not in header:
+                        text = text.replace(header,
+                          header[:9] + prefix + " " + header[9:], 1)
+                        break
+            # deal with the case where there was no Subject in the original
+            # mail (broken mailer?)
+            if not has_subject:
+                text = text.replace("\n\n", "Subject: " + prefix + "\n\n", 1)
+
+        return text
+
     def post_into_moderate(self, poster, dict, text):
         id = self.moderation_box.add(poster, text)
         recipients = self.moderators()