X-Git-Url: https://git.sommitrealweird.co.uk/eoc.git/blobdiff_plain/0f7cd8b5551e2ec4333cb10561375872dd7e03d3..26590c8e3b16016e8b136c2fa92297d66fa9a593:/eoc.py diff --git a/eoc.py b/eoc.py index b47b147..e328a4a 100644 --- 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.1.5" +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 @@ -874,14 +928,19 @@ class MailingList: if "base64" in text or "BASE64" in text: import StringIO for line in StringIO.StringIO(text): - if line.lower.beginswith("content-transfer-encoding:") and \ + if line.lower().startswith("content-transfer-encoding:") and \ "base64" in line.lower(): return text 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()