+"""Mailing list manager.
+
+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"
+PLUGIN_INTERFACE_VERSION = "1"
+
+import getopt
+import md5
+import os
+import shutil
+import smtplib
+import string
+import sys
+import time
+import ConfigParser
+try:
+ import email.Header
+ have_email_module = 1
+except ImportError:
+ have_email_module = 0
+import imp
+
+import qmqp
+
+
+# The following values will be overriden by "make install".
+TEMPLATE_DIRS = ["./templates"]
+DOTDIR = "dot-eoc"
+
+
+class EocException(Exception):
+
+ def __init__(self, arg=None):
+ self.msg = repr(arg)
+
+ def __str__(self):
+ return self.msg
+
+class UnknownList(EocException):
+ def __init__(self, list_name):
+ self.msg = "%s is not a known mailing list" % list_name
+
+class BadCommandAddress(EocException):
+ def __init__(self, address):
+ self.msg = "%s is not a valid command address" % address
+
+class BadSignature(EocException):
+ def __init__(self, address):
+ self.msg = "address %s has an invalid digital signature" % address
+
+class ListExists(EocException):
+ def __init__(self, list_name):
+ self.msg = "Mailing list %s alreadys exists" % list_name
+
+class ListDoesNotExist(EocException):
+ def __init__(self, list_name):
+ self.msg = "Mailing list %s does not exist" % list_name
+
+class MissingEnvironmentVariable(EocException):
+ def __init__(self, name):
+ self.msg = "Environment variable %s does not exist" % name
+
+class MissingTemplate(EocException):
+ def __init__(self, template):
+ self.msg = "Template %s does not exit" % template
+
+
+# Names of commands EoC recognizes in e-mail addresses.
+SIMPLE_COMMANDS = ["help", "list", "owner", "setlist", "setlistsilently", "ignore"]
+SUB_COMMANDS = ["subscribe", "unsubscribe"]
+HASH_COMMANDS = ["subyes", "subapprove", "subreject", "unsubyes",
+ "bounce", "probe", "approve", "reject", "setlistyes",
+ "setlistsilentyes"]
+COMMANDS = SIMPLE_COMMANDS + SUB_COMMANDS + HASH_COMMANDS
+
+
+def md5sum_as_hex(s):
+ return md5.new(s).hexdigest()
+
+environ = None
+
+def set_environ(new_environ):
+ global environ
+ environ = new_environ
+
+def get_from_environ(key):
+ global environ
+ if environ:
+ env = environ
+ else:
+ env = os.environ
+ if env.has_key(key):
+ return env[key].lower()
+ raise MissingEnvironmentVariable(key)
+
+class AddressParser:
+
+ """A parser for incoming e-mail addresses."""
+
+ def __init__(self, lists):
+ self.set_lists(lists)
+ self.set_skip_prefix(None)
+ self.set_forced_domain(None)
+
+ def set_lists(self, lists):
+ """Set the list of canonical list names we should know about."""
+ self.lists = lists
+
+ def set_skip_prefix(self, skip_prefix):
+ """Set the prefix to be removed from an address."""
+ self.skip_prefix = skip_prefix
+
+ def set_forced_domain(self, forced_domain):
+ """Set the domain part we should force the address to have."""
+ self.forced_domain = forced_domain
+
+ def clean(self, address):
+ """Remove cruft from the address and convert the rest to lower case."""
+ if self.skip_prefix:
+ n = self.skip_prefix and len(self.skip_prefix)
+ if address[:n] == self.skip_prefix:
+ address = address[n:]
+ if self.forced_domain:
+ parts = address.split("@", 1)
+ address = "%s@%s" % (parts[0], self.forced_domain)
+ return address.lower()
+
+ def split_address(self, address):
+ """Split an address to a local part and a domain."""
+ parts = address.lower().split("@", 1)
+ if len(parts) != 2:
+ return (address, "")
+ else:
+ return parts
+
+ # Does an address refer to a list? If not, return None, else return a list
+ # of additional parts (separated by hyphens) in the address. Note that []
+ # is not the same as None.
+
+ def additional_address_parts(self, address, listname):
+ addr_local, addr_domain = self.split_address(address)
+ list_local, list_domain = self.split_address(listname)
+
+ if addr_domain != list_domain:
+ return None
+
+ if addr_local.lower() == list_local.lower():
+ return []
+
+ n = len(list_local)
+ if addr_local[:n] != list_local or addr_local[n] != "-":
+ return None
+
+ return addr_local[n+1:].split("-")
+
+
+ # Parse an address we have received that identifies a list we manage.
+ # The address may contain command and signature parts. Return the name
+ # of the list, and a sequence of the additional parts (split at hyphens).
+ # Raise exceptions for errors. Note that the command will be valid, but
+ # cryptographic signatures in the address is not checked.
+
+ def parse(self, address):
+ address = self.clean(address)
+ for listname in self.lists:
+ parts = self.additional_address_parts(address, listname)
+ if parts == None:
+ pass
+ elif parts == []:
+ return listname, parts
+ elif parts[0] in HASH_COMMANDS:
+ if len(parts) != 3:
+ raise BadCommandAddress(address)
+ return listname, parts
+ elif parts[0] in COMMANDS:
+ return listname, parts
+
+ raise UnknownList(address)
+
+
+class MailingListManager:
+
+ def __init__(self, dotdir, sendmail="/usr/sbin/sendmail", lists=[],
+ smtp_server=None, qmqp_server=None):
+ self.dotdir = dotdir
+ self.sendmail = sendmail
+ self.smtp_server = smtp_server
+ self.qmqp_server = qmqp_server
+
+ self.make_dotdir()
+ self.secret = self.make_and_read_secret()
+
+ if not lists:
+ lists = filter(lambda s: "@" in s, os.listdir(dotdir))
+ self.set_lists(lists)
+
+ self.simple_commands = ["help", "list", "owner", "setlist",
+ "setlistsilently", "ignore"]
+ self.sub_commands = ["subscribe", "unsubscribe"]
+ self.hash_commands = ["subyes", "subapprove", "subreject", "unsubyes",
+ "bounce", "probe", "approve", "reject",
+ "setlistyes", "setlistsilentyes"]
+ self.commands = self.simple_commands + self.sub_commands + \
+ self.hash_commands
+
+ self.environ = None
+
+ self.load_plugins()
+
+ # Create the dot directory for us, if it doesn't exist already.
+ def make_dotdir(self):
+ if not os.path.isdir(self.dotdir):
+ os.makedirs(self.dotdir, 0700)
+
+ # Create the "secret" file, with a random value used as cookie for
+ # verification addresses.
+ def make_and_read_secret(self):
+ secret_name = os.path.join(self.dotdir, "secret")
+ if not os.path.isfile(secret_name):
+ f = open("/dev/urandom", "r")
+ secret = f.read(32)
+ f.close()
+ f = open(secret_name, "w")
+ f.write(secret)
+ f.close()
+ else:
+ f = open(secret_name, "r")
+ secret = f.read()
+ f.close()
+ return secret
+
+ # Load the plugins from DOTDIR/plugins/*.py.
+ def load_plugins(self):
+ self.plugins = []
+
+ dirname = os.path.join(DOTDIR, "plugins")
+ try:
+ plugins = os.listdir(dirname)
+ except OSError:
+ return
+
+ plugins.sort()
+ plugins = map(os.path.splitext, plugins)
+ plugins = filter(lambda p: p[1] == ".py", plugins)
+ plugins = map(lambda p: p[0], plugins)
+ for name in plugins:
+ pathname = os.path.join(dirname, name + ".py")
+ f = open(pathname, "r")
+ module = imp.load_module(name, f, pathname,
+ (".py", "r", imp.PY_SOURCE))
+ f.close()
+ if module.PLUGIN_INTERFACE_VERSION == PLUGIN_INTERFACE_VERSION:
+ self.plugins.append(module)
+
+ # Call function named funcname (a string) in all plugins, giving as
+ # arguments all the remaining arguments preceded by ml. Return value
+ # of each function is the new list of arguments to the next function.
+ # Return value of this function is the return value of the last function.
+ def call_plugins(self, funcname, list, *args):
+ for plugin in self.plugins:
+ if plugin.__dict__.has_key(funcname):
+ args = apply(plugin.__dict__[funcname], (list,) + args)
+ if type(args) != type((0,)):
+ args = (args,)
+ return args
+
+ # Set the list of listnames. The list of lists needs to be sorted in
+ # length order so that test@example.com is matched before
+ # test-list@example.com
+ def set_lists(self, lists):
+ temp = map(lambda s: (len(s), s), lists)
+ temp.sort()
+ self.lists = map(lambda t: t[1], temp)
+
+ # Return the list of listnames.
+ def get_lists(self):
+ return self.lists
+
+ # Decode an address that has been encoded to be part of a local part.
+ def decode_address(self, parts):
+ return string.join(string.join(parts, "-").split("="), "@")
+
+ # Is local_part@domain an existing list?
+ def is_list_name(self, local_part, domain):
+ return ("%s@%s" % (local_part, domain)) in self.lists
+
+ # Compute the verification checksum for an address.
+ def compute_hash(self, address):
+ return md5sum_as_hex(address + self.secret)
+
+ # Is the verification signature in a parsed address bad? If so, return true,
+ # otherwise return false.
+ def signature_is_bad(self, dict, hash):
+ local_part, domain = dict["name"].split("@")
+ address = "%s-%s-%s@%s" % (local_part, dict["command"], dict["id"],
+ domain)
+ correct = self.compute_hash(address)
+ return correct != hash
+
+ # Parse a command address we have received and check its validity
+ # (including signature, if any). Return a dictionary with keys
+ # "command", "sender" (address that was encoded into address, if
+ # any), "id" (group ID).
+
+ def parse_recipient_address(self, address, skip_prefix, forced_domain):
+ ap = AddressParser(self.get_lists())
+ ap.set_lists(self.get_lists())
+ ap.set_skip_prefix(skip_prefix)
+ ap.set_forced_domain(forced_domain)
+ listname, parts = ap.parse(address)
+
+ dict = { "name": listname }
+
+ if parts == []:
+ dict["command"] = "post"
+ else:
+ command, args = parts[0], parts[1:]
+ dict["command"] = command
+ if command in SUB_COMMANDS:
+ dict["sender"] = self.decode_address(args)
+ elif command in HASH_COMMANDS:
+ dict["id"] = args[0]
+ hash = args[1]
+ if self.signature_is_bad(dict, hash):
+ raise BadSignature(address)
+
+ return dict
+
+ # Does an address refer to a mailing list?
+ def is_list(self, name, skip_prefix=None, domain=None):
+ try:
+ self.parse_recipient_address(name, skip_prefix, domain)
+ except BadCommandAddress:
+ return 0
+ except BadSignature:
+ return 0
+ except UnknownList:
+ return 0
+ return 1
+
+ # Create a new list and return it.
+ def create_list(self, name):
+ if self.is_list(name):
+ raise ListExists(name)
+ self.set_lists(self.lists + [name])
+ return MailingList(self, name)
+
+ # Open an existing list.
+ def open_list(self, name):
+ if self.is_list(name):
+ return self.open_list_exact(name)
+ else:
+ x = name + "@"
+ for list in self.lists:
+ if list[:len(x)] == x:
+ return self.open_list_exact(list)
+ raise ListDoesNotExist(name)
+
+ def open_list_exact(self, name):
+ for list in self.get_lists():
+ if list.lower() == name.lower():
+ return MailingList(self, list)
+ raise ListDoesNotExist(name)
+
+ # Process an incoming message.
+ def incoming_message(self, skip_prefix, domain, moderate, post):
+ debug("Processing incoming message.")
+ debug("$SENDER = <%s>" % get_from_environ("SENDER"))
+ debug("$RECIPIENT = <%s>" % get_from_environ("RECIPIENT"))
+ dict = self.parse_recipient_address(get_from_environ("RECIPIENT"),
+ skip_prefix,
+ domain)
+ dict["force-moderation"] = moderate
+ dict["force-posting"] = post
+ debug("List is <%(name)s>, command is <%(command)s>." % dict)
+ list = self.open_list_exact(dict["name"])
+ list.obey(dict)
+
+ # Clean up bouncing address and do other janitorial work for all lists.
+ def cleaning_woman(self, send_mail=None):
+ now = time.time()
+ for listname in self.lists:
+ list = self.open_list_exact(listname)
+ if send_mail:
+ list.send_mail = send_mail
+ list.cleaning_woman(now)
+
+ # Send a mail to the desired recipients.
+ def send_mail(self, envelope_sender, recipients, text):
+ debug("send_mail:\n sender=%s\n recipients=%s\n text=\n %s" %
+ (envelope_sender, str(recipients),
+ "\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()
+ elif self.qmqp_server:
+ q = qmqp.QMQP(self.qmqp_server)
+ q.sendmail(envelope_sender, recipients, text)
+ q.quit()
+ else:
+ recipients = string.join(recipients, " ")
+ f = os.popen("%s -oi -f '%s' %s" %
+ (self.sendmail,
+ envelope_sender,
+ recipients),
+ "w")
+ f.write(text)
+ f.close()
+ else:
+ debug("send_mail: no recipients, not sending")
+
+
+
+class MailingList:
+
+ posting_opts = ["auto", "free", "moderated"]
+
+ def __init__(self, mlm, name):
+ self.mlm = mlm
+ self.name = name
+
+ self.cp = ConfigParser.ConfigParser()
+ self.cp.add_section("list")
+ self.cp.set("list", "owners", "")
+ self.cp.set("list", "moderators", "")
+ self.cp.set("list", "subscription", "free")
+ self.cp.set("list", "posting", "free")
+ self.cp.set("list", "archived", "no")
+ self.cp.set("list", "mail-on-subscription-changes", "no")
+ self.cp.set("list", "mail-on-forced-unsubscribe", "no")
+ self.cp.set("list", "ignore-bounce", "no")
+ self.cp.set("list", "language", "")
+ self.cp.set("list", "pristine-headers", "")
+
+ self.dirname = os.path.join(self.mlm.dotdir, name)
+ self.make_listdir()
+ self.cp.read(self.mkname("config"))
+
+ self.subscribers = SubscriberDatabase(self.dirname, "subscribers")
+ self.moderation_box = MessageBox(self.dirname, "moderation-box")
+ self.subscription_box = MessageBox(self.dirname, "subscription-box")
+ self.bounce_box = MessageBox(self.dirname, "bounce-box")
+
+ def make_listdir(self):
+ if not os.path.isdir(self.dirname):
+ os.mkdir(self.dirname, 0700)
+ self.save_config()
+ f = open(self.mkname("subscribers"), "w")
+ f.close()
+
+ def mkname(self, relative):
+ return os.path.join(self.dirname, relative)
+
+ def save_config(self):
+ f = open(self.mkname("config"), "w")
+ self.cp.write(f)
+ f.close()
+
+ def read_stdin(self):
+ data = sys.stdin.read()
+ # Skip Unix mbox "From " mail start indicator
+ if data[:5] == "From ":
+ data = string.split(data, "\n", 1)[1]
+ return data
+
+ def invent_boundary(self):
+ return "%s/%s" % (md5sum_as_hex(str(time.time())),
+ md5sum_as_hex(self.name))
+
+ def command_address(self, command):
+ local_part, domain = self.name.split("@")
+ return "%s-%s@%s" % (local_part, command, domain)
+
+ def signed_address(self, command, id):
+ unsigned = self.command_address("%s-%s" % (command, id))
+ hash = self.mlm.compute_hash(unsigned)
+ return self.command_address("%s-%s-%s" % (command, id, hash))
+
+ def ignore(self):
+ return self.command_address("ignore")
+
+ def nice_7bit(self, str):
+ for c in str:
+ if (ord(c) < 32 and not c.isspace()) or ord(c) >= 127:
+ return False
+ 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)
+ 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
+
+ def template(self, template_name, dict):
+ lang = self.cp.get("list", "language")
+ if lang:
+ template_name_lang = template_name + "." + lang
+ else:
+ template_name_lang = template_name
+
+ if not dict.has_key("list"):
+ dict["list"] = self.name
+ dict["local"], dict["domain"] = self.name.split("@")
+ if not dict.has_key("list"):
+ dict["list"] = self.name
+
+ for dir in [os.path.join(self.dirname, "templates")] + TEMPLATE_DIRS:
+ pathname = os.path.join(dir, template_name_lang)
+ if not os.path.exists(pathname):
+ pathname = os.path.join(dir, template_name)
+ if os.path.exists(pathname):
+ f = open(pathname, "r")
+ data = f.read()
+ f.close()
+ return data % dict
+
+ raise MissingTemplate(template_name)
+
+ def send_template(self, envelope_sender, sender, recipients,
+ template_name, dict):
+ dict["From"] = "EoC <%s>" % sender
+ dict["To"] = string.join(recipients, ", ")
+ text = self.template(template_name, dict)
+ if not text:
+ return
+ if self.cp.get("list", "pristine-headers") != "yes":
+ text = self.mime_encode_headers(text)
+ self.mlm.send_mail(envelope_sender, recipients, text)
+
+ def send_info_message(self, recipients, template_name, dict):
+ self.send_template(self.command_address("ignore"),
+ self.command_address("help"),
+ recipients,
+ template_name,
+ dict)
+
+ def owners(self):
+ return self.cp.get("list", "owners").split()
+
+ def moderators(self):
+ return self.cp.get("list", "moderators").split()
+
+ def is_list_owner(self, address):
+ return address in self.owners()
+
+ def obey_help(self):
+ self.send_info_message([get_from_environ("SENDER")], "help", {})
+
+ def obey_list(self):
+ recipient = get_from_environ("SENDER")
+ if self.is_list_owner(recipient):
+ addr_list = self.subscribers.get_all()
+ addr_text = string.join(addr_list, "\n")
+ self.send_info_message([recipient], "list",
+ {
+ "addresses": addr_text,
+ "count": len(addr_list),
+ })
+ else:
+ self.send_info_message([recipient], "list-sorry", {})
+
+ def obey_setlist(self, origmail):
+ recipient = get_from_environ("SENDER")
+ if self.is_list_owner(recipient):
+ id = self.moderation_box.add(recipient, origmail)
+ if self.parse_setlist_addresses(origmail) == None:
+ self.send_bad_addresses_in_setlist(id)
+ self.moderation_box.remove(id)
+ else:
+ confirm = self.signed_address("setlistyes", id)
+ self.send_info_message(self.owners(), "setlist-confirm",
+ {
+ "confirm": confirm,
+ "origmail": origmail,
+ "boundary": self.invent_boundary(),
+ })
+
+ else:
+ self.send_info_message([recipient], "setlist-sorry", {})
+
+ def obey_setlistsilently(self, origmail):
+ recipient = get_from_environ("SENDER")
+ if self.is_list_owner(recipient):
+ id = self.moderation_box.add(recipient, origmail)
+ if self.parse_setlist_addresses(origmail) == None:
+ self.send_bad_addresses_in_setlist(id)
+ self.moderation_box.remove(id)
+ else:
+ confirm = self.signed_address("setlistsilentyes", id)
+ self.send_info_message(self.owners(), "setlist-confirm",
+ {
+ "confirm": confirm,
+ "origmail": origmail,
+ "boundary": self.invent_boundary(),
+ })
+ else:
+ self.info_message([recipient], "setlist-sorry", {})
+
+ def parse_setlist_addresses(self, text):
+ body = text.split("\n\n", 1)[1]
+ lines = body.split("\n")
+ lines = filter(lambda line: line != "", lines)
+ badlines = filter(lambda line: "@" not in line, lines)
+ if badlines:
+ return None
+ else:
+ return lines
+
+ def send_bad_addresses_in_setlist(self, id):
+ addr = self.moderation_box.get_address(id)
+ origmail = self.moderation_box.get(id)
+ self.send_info_message([addr], "setlist-badlist",
+ {
+ "origmail": origmail,
+ "boundary": self.invent_boundary(),
+ })
+
+
+ def obey_setlistyes(self, dict):
+ if self.moderation_box.has(dict["id"]):
+ text = self.moderation_box.get(dict["id"])
+ addresses = self.parse_setlist_addresses(text)
+ if addresses == None:
+ self.send_bad_addresses_in_setlist(id)
+ else:
+ removed_subscribers = []
+ self.subscribers.lock()
+ old = self.subscribers.get_all()
+ for address in old:
+ if address.lower() not in map(string.lower, addresses):
+ self.subscribers.remove(address)
+ removed_subscribers.append(address)
+ else:
+ for x in addresses:
+ if x.lower() == address.lower():
+ addresses.remove(x)
+ self.subscribers.add_many(addresses)
+ self.subscribers.save()
+
+ for recipient in addresses:
+ self.send_info_message([recipient], "sub-welcome", {})
+ for recipient in removed_subscribers:
+ self.send_info_message([recipient], "unsub-goodbye", {})
+ self.send_info_message(self.owners(), "setlist-done", {})
+
+ self.moderation_box.remove(dict["id"])
+
+ def obey_setlistsilentyes(self, dict):
+ if self.moderation_box.has(dict["id"]):
+ text = self.moderation_box.get(dict["id"])
+ addresses = self.parse_setlist_addresses(text)
+ if addresses == None:
+ self.send_bad_addresses_in_setlist(id)
+ else:
+ self.subscribers.lock()
+ old = self.subscribers.get_all()
+ for address in old:
+ if address not in addresses:
+ self.subscribers.remove(address)
+ else:
+ addresses.remove(address)
+ self.subscribers.add_many(addresses)
+ self.subscribers.save()
+ self.send_info_message(self.owners(), "setlist-done", {})
+
+ self.moderation_box.remove(dict["id"])
+
+ def obey_owner(self, text):
+ sender = get_from_environ("SENDER")
+ recipients = self.cp.get("list", "owners").split()
+ self.mlm.send_mail(sender, recipients, text)
+
+ def obey_subscribe_or_unsubscribe(self, dict, template_name, command,
+ origmail):
+
+ requester = get_from_environ("SENDER")
+ subscriber = dict["sender"]
+ if not subscriber:
+ subscriber = requester
+ if subscriber.find("@") == -1:
+ info("Trying to (un)subscribe address without @: %s" % subscriber)
+ return
+ if self.cp.get("list", "ignore-bounce") == "yes":
+ info("Will not (un)subscribe address: %s from static list" %subscriber)
+ return
+ if requester in self.owners():
+ confirmers = self.owners()
+ else:
+ confirmers = [subscriber]
+
+ id = self.subscription_box.add(subscriber, origmail)
+ confirm = self.signed_address(command, id)
+ self.send_info_message(confirmers, template_name,
+ {
+ "confirm": confirm,
+ "origmail": origmail,
+ "boundary": self.invent_boundary(),
+ })
+
+ def obey_subscribe(self, dict, origmail):
+ self.obey_subscribe_or_unsubscribe(dict, "sub-confirm", "subyes",
+ origmail)
+
+ def obey_unsubscribe(self, dict, origmail):
+ self.obey_subscribe_or_unsubscribe(dict, "unsub-confirm", "unsubyes",
+ origmail)
+
+ def obey_subyes(self, dict):
+ if self.subscription_box.has(dict["id"]):
+ if self.cp.get("list", "subscription") == "free":
+ recipient = self.subscription_box.get_address(dict["id"])
+ self.subscribers.lock()
+ self.subscribers.add(recipient)
+ self.subscribers.save()
+ sender = self.command_address("help")
+ self.send_template(self.ignore(), sender, [recipient],
+ "sub-welcome", {})
+ self.subscription_box.remove(dict["id"])
+ if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+ self.send_info_message(self.owners(),
+ "sub-owner-notification",
+ {
+ "address": recipient,
+ })
+ else:
+ recipients = self.cp.get("list", "owners").split()
+ confirm = self.signed_address("subapprove", dict["id"])
+ deny = self.signed_address("subreject", dict["id"])
+ subscriber = self.subscription_box.get_address(dict["id"])
+ origmail = self.subscription_box.get(dict["id"])
+ self.send_template(self.ignore(), deny, recipients,
+ "sub-moderate",
+ {
+ "confirm": confirm,
+ "deny": deny,
+ "subscriber": subscriber,
+ "origmail": origmail,
+ "boundary": self.invent_boundary(),
+ })
+ recipient = self.subscription_box.get_address(dict["id"])
+ self.send_info_message([recipient], "sub-wait", {})
+
+ def obey_subapprove(self, dict):
+ if self.subscription_box.has(dict["id"]):
+ recipient = self.subscription_box.get_address(dict["id"])
+ self.subscribers.lock()
+ self.subscribers.add(recipient)
+ self.subscribers.save()
+ self.send_info_message([recipient], "sub-welcome", {})
+ self.subscription_box.remove(dict["id"])
+ if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+ self.send_info_message(self.owners(), "sub-owner-notification",
+ {
+ "address": recipient,
+ })
+
+ def obey_subreject(self, dict):
+ if self.subscription_box.has(dict["id"]):
+ recipient = self.subscription_box.get_address(dict["id"])
+ self.send_info_message([recipient], "sub-reject", {})
+ self.subscription_box.remove(dict["id"])
+
+ def obey_unsubyes(self, dict):
+ if self.subscription_box.has(dict["id"]):
+ recipient = self.subscription_box.get_address(dict["id"])
+ self.subscribers.lock()
+ self.subscribers.remove(recipient)
+ self.subscribers.save()
+ self.send_info_message([recipient], "unsub-goodbye", {})
+ self.subscription_box.remove(dict["id"])
+ if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+ self.send_info_message(self.owners(),
+ "unsub-owner-notification",
+ {
+ "address": recipient,
+ })
+
+ def store_into_archive(self, text):
+ if self.cp.get("list", "archived") == "yes":
+ archdir = os.path.join(self.dirname, "archive")
+ if not os.path.exists(archdir):
+ os.mkdir(archdir, 0700)
+ id = md5sum_as_hex(text)
+ f = open(os.path.join(archdir, id), "w")
+ f.write(text)
+ f.close()
+
+ def list_headers(self):
+ local, domain = self.name.split("@")
+ list = []
+ list.append("List-Id: <%s.%s>" % (local, domain))
+ list.append("List-Help: <mailto:%s-help@%s>" % (local, domain))
+ list.append("List-Unsubscribe: <mailto:%s-unsubscribe@%s>" %
+ (local, domain))
+ list.append("List-Subscribe: <mailto:%s-subscribe@%s>" %
+ (local, domain))
+ list.append("List-Post: <mailto:%s@%s>" % (local, domain))
+ list.append("List-Owner: <mailto:%s-owner@%s>" % (local, domain))
+ list.append("Precedence: bulk");
+ return string.join(list, "\n") + "\n"
+
+ def read_file(self, basename):
+ try:
+ f = open(os.path.join(self.dirname, basename), "r")
+ data = f.read()
+ f.close()
+ return data
+ except IOError:
+ return ""
+
+ def headers_to_add(self):
+ headers_to_add = self.read_file("headers-to-add").rstrip()
+ if headers_to_add:
+ return headers_to_add + "\n"
+ else:
+ return ""
+
+ def remove_some_headers(self, mail, headers_to_remove):
+ endpos = mail.find("\n\n")
+ if endpos == -1:
+ endpos = mail.find("\n\r\n")
+ if endpos == -1:
+ return mail
+ headers = mail[:endpos].split("\n")
+ body = mail[endpos:]
+
+ remaining = []
+ add_continuation_lines = 0
+ for header in headers:
+ pos = header.find(":")
+ if pos == -1:
+ if add_continuation_lines:
+ remaining.append(header)
+ else:
+ name = header[:pos].lower()
+ if name in headers_to_remove:
+ add_continuation_lines = 0
+ else:
+ add_continuation_lines = 1
+ remaining.append(header)
+
+ return "\n".join(remaining) + body
+
+ def headers_to_remove(self, text):
+ headers_to_remove = self.read_file("headers-to-remove").split("\n")
+ headers_to_remove = map(lambda s: s.strip().lower(),
+ headers_to_remove)
+ return self.remove_some_headers(text, headers_to_remove)
+
+ def append_footer(self, text):
+ if "base64" in text or "BASE64" in text:
+ import StringIO
+ for line in StringIO.StringIO(text):
+ if line.lower.beginswith("content-transfer-encoding:") and \
+ "base64" in line.lower():
+ return text
+ return text + self.template("footer", {})
+
+ def send_mail_to_subscribers(self, text):
+ text = self.headers_to_add() + self.list_headers() + \
+ self.headers_to_remove(text)
+ text = self.append_footer(text)
+ text, = self.mlm.call_plugins("send_mail_to_subscribers_hook",
+ self, text)
+ if have_email_module and \
+ self.cp.get("list", "pristine-headers") != "yes":
+ text = self.mime_encode_headers(text)
+ self.store_into_archive(text)
+ for group in self.subscribers.groups():
+ bounce = self.signed_address("bounce", group)
+ addresses = self.subscribers.in_group(group)
+ self.mlm.send_mail(bounce, addresses, text)
+
+ def post_into_moderate(self, poster, dict, text):
+ id = self.moderation_box.add(poster, text)
+ recipients = self.moderators()
+ if recipients == []:
+ recipients = self.owners()
+
+ confirm = self.signed_address("approve", id)
+ deny = self.signed_address("reject", id)
+ self.send_template(self.ignore(), deny, recipients, "msg-moderate",
+ {
+ "confirm": confirm,
+ "deny": deny,
+ "origmail": text,
+ "boundary": self.invent_boundary(),
+ })
+ self.send_info_message([poster], "msg-wait", {})
+
+ def should_be_moderated(self, posting, poster):
+ if posting == "moderated":
+ return 1
+ if posting == "auto":
+ if poster.lower() not in \
+ map(string.lower, self.subscribers.get_all()):
+ return 1
+ return 0
+
+ def obey_post(self, dict, text):
+ if dict.has_key("force-moderation") and dict["force-moderation"]:
+ force_moderation = 1
+ else:
+ force_moderation = 0
+ if dict.has_key("force-posting") and dict["force-posting"]:
+ force_posting = 1
+ else:
+ force_posting = 0
+ posting = self.cp.get("list", "posting")
+ if posting not in self.posting_opts:
+ error("You have a weird 'posting' config. Please, review it")
+ poster = get_from_environ("SENDER")
+ if force_moderation:
+ self.post_into_moderate(poster, dict, text)
+ elif force_posting:
+ self.send_mail_to_subscribers(text)
+ elif self.should_be_moderated(posting, poster):
+ self.post_into_moderate(poster, dict, text)
+ else:
+ self.send_mail_to_subscribers(text)
+
+ def obey_approve(self, dict):
+ if self.moderation_box.lock(dict["id"]):
+ if self.moderation_box.has(dict["id"]):
+ text = self.moderation_box.get(dict["id"])
+ self.send_mail_to_subscribers(text)
+ self.moderation_box.remove(dict["id"])
+ self.moderation_box.unlock(dict["id"])
+
+ def obey_reject(self, dict):
+ if self.moderation_box.lock(dict["id"]):
+ if self.moderation_box.has(dict["id"]):
+ self.moderation_box.remove(dict["id"])
+ self.moderation_box.unlock(dict["id"])
+
+ def split_address_list(self, addrs):
+ domains = {}
+ for addr in addrs:
+ userpart, domain = addr.split("@")
+ if domains.has_key(domain):
+ domains[domain].append(addr)
+ else:
+ domains[domain] = [addr]
+ result = []
+ if len(domains.keys()) == 1:
+ for addr in addrs:
+ result.append([addr])
+ else:
+ result = domains.values()
+ return result
+
+ def obey_bounce(self, dict, text):
+ if self.subscribers.has_group(dict["id"]):
+ self.subscribers.lock()
+ addrs = self.subscribers.in_group(dict["id"])
+ if len(addrs) == 1:
+ if self.cp.get("list", "ignore-bounce") == "yes":
+ info("Address <%s> bounced, ignoring bounce as configured." %
+ addrs[0])
+ self.subscribers.unlock()
+ return
+ debug("Address <%s> bounced, setting state to bounce." %
+ addrs[0])
+ bounce_id = self.bounce_box.add(addrs[0], text[:4096])
+ self.subscribers.set(dict["id"], "status", "bounced")
+ self.subscribers.set(dict["id"], "timestamp-bounced",
+ "%f" % time.time())
+ self.subscribers.set(dict["id"], "bounce-id",
+ bounce_id)
+ else:
+ debug("Group %s bounced, splitting." % dict["id"])
+ for new_addrs in self.split_address_list(addrs):
+ self.subscribers.add_many(new_addrs)
+ self.subscribers.remove_group(dict["id"])
+ self.subscribers.save()
+ else:
+ debug("Ignoring bounce, group %s doesn't exist (anymore?)." %
+ dict["id"])
+
+ def obey_probe(self, dict, text):
+ id = dict["id"]
+ if self.subscribers.has_group(id):
+ self.subscribers.lock()
+ if self.subscribers.get(id, "status") == "probed":
+ self.subscribers.set(id, "status", "probebounced")
+ self.subscribers.save()
+
+ def obey(self, dict):
+ text = self.read_stdin()
+
+ if dict["command"] in ["help", "list", "subscribe", "unsubscribe",
+ "subyes", "subapprove", "subreject",
+ "unsubyes", "post", "approve"]:
+ sender = get_from_environ("SENDER")
+ if not sender:
+ debug("Ignoring bounce message for %s command." %
+ dict["command"])
+ return
+
+ if dict["command"] == "help":
+ self.obey_help()
+ elif dict["command"] == "list":
+ self.obey_list()
+ elif dict["command"] == "owner":
+ self.obey_owner(text)
+ elif dict["command"] == "subscribe":
+ self.obey_subscribe(dict, text)
+ elif dict["command"] == "unsubscribe":
+ self.obey_unsubscribe(dict, text)
+ elif dict["command"] == "subyes":
+ self.obey_subyes(dict)
+ elif dict["command"] == "subapprove":
+ self.obey_subapprove(dict)
+ elif dict["command"] == "subreject":
+ self.obey_subreject(dict)
+ elif dict["command"] == "unsubyes":
+ self.obey_unsubyes(dict)
+ elif dict["command"] == "post":
+ self.obey_post(dict, text)
+ elif dict["command"] == "approve":
+ self.obey_approve(dict)
+ elif dict["command"] == "reject":
+ self.obey_reject(dict)
+ elif dict["command"] == "bounce":
+ self.obey_bounce(dict, text)
+ elif dict["command"] == "probe":
+ self.obey_probe(dict, text)
+ elif dict["command"] == "setlist":
+ self.obey_setlist(text)
+ elif dict["command"] == "setlistsilently":
+ self.obey_setlistsilently(text)
+ elif dict["command"] == "setlistyes":
+ self.obey_setlistyes(dict)
+ elif dict["command"] == "setlistsilentyes":
+ self.obey_setlistsilentyes(dict)
+ elif dict["command"] == "ignore":
+ pass
+
+ def get_bounce_text(self, id):
+ bounce_id = self.subscribers.get(id, "bounce-id")
+ if self.bounce_box.has(bounce_id):
+ bounce_text = self.bounce_box.get(bounce_id)
+ bounce_text = string.join(map(lambda s: "> " + s + "\n",
+ bounce_text.split("\n")), "")
+ else:
+ bounce_text = "Bounce message not available."
+ return bounce_text
+
+ one_week = 7.0 * 24.0 * 60.0 * 60.0
+
+ def handle_bounced_groups(self, now):
+ for id in self.subscribers.groups():
+ status = self.subscribers.get(id, "status")
+ t = float(self.subscribers.get(id, "timestamp-bounced"))
+ if status == "bounced":
+ if now - t > self.one_week:
+ sender = self.signed_address("probe", id)
+ recipients = self.subscribers.in_group(id)
+ self.send_template(sender, sender, recipients,
+ "bounce-warning", {
+ "bounce": self.get_bounce_text(id),
+ "boundary": self.invent_boundary(),
+ })
+ self.subscribers.set(id, "status", "probed")
+ elif status == "probed":
+ if now - t > 2 * self.one_week:
+ debug(("Cleaning woman: probe didn't bounce " +
+ "for group <%s>, setting status to ok.") % id)
+ self.subscribers.set(id, "status", "ok")
+ self.bounce_box.remove(
+ self.subscribers.get(id, "bounce-id"))
+ elif status == "probebounced":
+ sender = self.command_address("help")
+ for address in self.subscribers.in_group(id):
+ if self.cp.get("list", "mail-on-forced-unsubscribe") \
+ == "yes":
+ self.send_template(sender, sender,
+ self.owners(),
+ "bounce-owner-notification",
+ {
+ "address": address,
+ "bounce": self.get_bounce_text(id),
+ "boundary": self.invent_boundary(),
+ })
+
+ self.bounce_box.remove(
+ self.subscribers.get(id, "bounce-id"))
+ self.subscribers.remove(address)
+ debug("Cleaning woman: removing <%s>." % address)
+ self.send_template(sender, sender, [address],
+ "bounce-goodbye", {})
+
+ def join_nonbouncing_groups(self, now):
+ to_be_joined = []
+ for id in self.subscribers.groups():
+ status = self.subscribers.get(id, "status")
+ age1 = now - float(self.subscribers.get(id, "timestamp-bounced"))
+ age2 = now - float(self.subscribers.get(id, "timestamp-created"))
+ if status == "ok":
+ if age1 > self.one_week and age2 > self.one_week:
+ to_be_joined.append(id)
+ if to_be_joined:
+ addrs = []
+ for id in to_be_joined:
+ addrs = addrs + self.subscribers.in_group(id)
+ self.subscribers.add_many(addrs)
+ for id in to_be_joined:
+ self.bounce_box.remove(self.subscribers.get(id, "bounce-id"))
+ self.subscribers.remove_group(id)
+
+ def remove_empty_groups(self):
+ for id in self.subscribers.groups()[:]:
+ if len(self.subscribers.in_group(id)) == 0:
+ self.subscribers.remove_group(id)
+
+ def cleaning_woman(self, now):
+ if self.subscribers.lock():
+ self.handle_bounced_groups(now)
+ self.join_nonbouncing_groups(now)
+ self.subscribers.save()
+
+class SubscriberDatabase:
+
+ def __init__(self, dirname, name):
+ self.dict = {}
+ self.filename = os.path.join(dirname, name)
+ self.lockname = os.path.join(dirname, "lock")
+ self.loaded = 0
+ self.locked = 0
+
+ def lock(self):
+ if os.system("lockfile -l 60 %s" % self.lockname) == 0:
+ self.locked = 1
+ self.load()
+ return self.locked
+
+ def unlock(self):
+ os.remove(self.lockname)
+ self.locked = 0
+
+ def load(self):
+ if not self.loaded and not self.dict:
+ f = open(self.filename, "r")
+ for line in f.xreadlines():
+ parts = line.split()
+ self.dict[parts[0]] = {
+ "status": parts[1],
+ "timestamp-created": parts[2],
+ "timestamp-bounced": parts[3],
+ "bounce-id": parts[4],
+ "addresses": parts[5:],
+ }
+ f.close()
+ self.loaded = 1
+
+ def save(self):
+ assert self.locked
+ assert self.loaded
+ f = open(self.filename + ".new", "w")
+ for id in self.dict.keys():
+ f.write("%s " % id)
+ f.write("%s " % self.dict[id]["status"])
+ f.write("%s " % self.dict[id]["timestamp-created"])
+ f.write("%s " % self.dict[id]["timestamp-bounced"])
+ f.write("%s " % self.dict[id]["bounce-id"])
+ f.write("%s\n" % string.join(self.dict[id]["addresses"], " "))
+ f.close()
+ os.remove(self.filename)
+ os.rename(self.filename + ".new", self.filename)
+ self.unlock()
+
+ def get(self, id, attribute):
+ self.load()
+ if self.dict.has_key(id) and self.dict[id].has_key(attribute):
+ return self.dict[id][attribute]
+ return None
+
+ def set(self, id, attribute, value):
+ assert self.locked
+ self.load()
+ if self.dict.has_key(id) and self.dict[id].has_key(attribute):
+ self.dict[id][attribute] = value
+
+ def add(self, address):
+ return self.add_many([address])
+
+ def add_many(self, addresses):
+ assert self.locked
+ assert self.loaded
+ for addr in addresses[:]:
+ if addr.find("@") == -1:
+ info("Address '%s' does not contain an @, ignoring it." % addr)
+ addresses.remove(addr)
+ for id in self.dict.keys():
+ old_ones = self.dict[id]["addresses"]
+ for addr in addresses:
+ for x in old_ones:
+ if x.lower() == addr.lower():
+ old_ones.remove(x)
+ self.dict[id]["addresses"] = old_ones
+ id = self.new_group()
+ self.dict[id] = {
+ "status": "ok",
+ "timestamp-created": self.timestamp(),
+ "timestamp-bounced": "0",
+ "bounce-id": "..notexist..",
+ "addresses": addresses,
+ }
+ return id
+
+ def new_group(self):
+ keys = self.dict.keys()
+ if keys:
+ keys = map(lambda x: int(x), keys)
+ keys.sort()
+ return "%d" % (keys[-1] + 1)
+ else:
+ return "0"
+
+ def timestamp(self):
+ return "%.0f" % time.time()
+
+ def get_all(self):
+ self.load()
+ list = []
+ for values in self.dict.values():
+ list = list + values["addresses"]
+ return list
+
+ def groups(self):
+ self.load()
+ return self.dict.keys()
+
+ def has_group(self, id):
+ self.load()
+ return self.dict.has_key(id)
+
+ def in_group(self, id):
+ self.load()
+ return self.dict[id]["addresses"]
+
+ def remove(self, address):
+ assert self.locked
+ self.load()
+ for id in self.dict.keys():
+ group = self.dict[id]
+ for x in group["addresses"][:]:
+ if x.lower() == address.lower():
+ group["addresses"].remove(x)
+ if len(group["addresses"]) == 0:
+ del self.dict[id]
+
+ def remove_group(self, id):
+ assert self.locked
+ self.load()
+ del self.dict[id]
+
+
+class MessageBox:
+
+ def __init__(self, dirname, boxname):
+ self.boxdir = os.path.join(dirname, boxname)
+ if not os.path.isdir(self.boxdir):
+ os.mkdir(self.boxdir, 0700)
+
+ def filename(self, id):
+ return os.path.join(self.boxdir, id)
+
+ def add(self, address, message_text):
+ id = self.make_id(message_text)
+ filename = self.filename(id)
+ f = open(filename + ".address", "w")
+ f.write(address)
+ f.close()
+ f = open(filename + ".new", "w")
+ f.write(message_text)
+ f.close()
+ os.rename(filename + ".new", filename)
+ return id
+
+ def make_id(self, message_text):
+ return md5sum_as_hex(message_text)
+ # XXX this might be unnecessarily long
+
+ def remove(self, id):
+ filename = self.filename(id)
+ if os.path.isfile(filename):
+ os.remove(filename)
+ os.remove(filename + ".address")
+
+ def has(self, id):
+ return os.path.isfile(self.filename(id))
+
+ def get_address(self, id):
+ f = open(self.filename(id) + ".address", "r")
+ data = f.read()
+ f.close()
+ return data.strip()
+
+ def get(self, id):
+ f = open(self.filename(id), "r")
+ data = f.read()
+ f.close()
+ return data
+
+ def lockname(self, id):
+ return self.filename(id) + ".lock"
+
+ def lock(self, id):
+ if os.system("lockfile -l 600 %s" % self.lockname(id)) == 0:
+ return 1
+ else:
+ return 0
+
+ def unlock(self, id):
+ try:
+ os.remove(self.lockname(id))
+ except os.error:
+ pass
+
+
+
+class DevNull:
+
+ def write(self, str):
+ pass
+
+
+log_file_handle = None
+def log_file():
+ global log_file_handle
+ if log_file_handle is None:
+ try:
+ log_file_handle = open(os.path.join(DOTDIR, "logfile.txt"), "a")
+ except:
+ log_file_handle = DevNull()
+ return log_file_handle
+
+def timestamp():
+ tuple = time.localtime(time.time())
+ return time.strftime("%Y-%m-%d %H:%M:%S", tuple) + " [%d]" % os.getpid()
+
+
+quiet = 0
+
+
+# No logging to stderr of debug messages. Some MTAs have a limit on how
+# much data they accept via stderr and debug logs will fill that quickly.
+def debug(msg):
+ log_file().write(timestamp() + " " + msg + "\n")
+
+
+# Log to log file first, in case MTA's stderr buffer fills up and we lose
+# logs.
+def info(msg):
+ log_file().write(timestamp() + " " + msg + "\n")
+ sys.stderr.write(msg + "\n")
+
+
+def error(msg):
+ info(msg)
+ sys.exit(1)
+
+
+def usage():
+ sys.stdout.write("""\
+Usage: enemies-of-carlotta [options] command
+Mailing list manager.
+
+Options:
+ --name=listname@domain
+ --owner=address@domain
+ --moderator=address@domain
+ --subscription=free/moderated
+ --posting=free/moderated/auto
+ --archived=yes/no
+ --ignore-bounce=yes/no
+ --language=language code or empty
+ --mail-on-forced-unsubscribe=yes/no
+ --mail-on-subscription-changes=yes/no
+ --skip-prefix=string
+ --domain=domain.name
+ --smtp-server=domain.name
+ --quiet
+ --moderate
+
+Commands:
+ --help
+ --create
+ --subscribe
+ --unsubscribe
+ --list
+ --is-list
+ --edit
+ --incoming
+ --cleaning-woman
+ --show-lists
+
+For more detailed information, please read the enemies-of-carlotta(1)
+manual page.
+""")
+ sys.exit(0)
+
+
+def no_act_send_mail(sender, recipients, text):
+ print "NOT SENDING MAIL FOR REAL!"
+ print "Sender:", sender
+ print "Recipients:", recipients
+ print "Mail:"
+ print "\n".join(map(lambda s: " " + s, text.split("\n")))
+
+
+def set_list_options(list, owners, moderators, subscription, posting,
+ archived, language, ignore_bounce,
+ mail_on_sub_changes, mail_on_forced_unsub):
+ if owners:
+ list.cp.set("list", "owners", string.join(owners, " "))
+ if moderators:
+ list.cp.set("list", "moderators", string.join(moderators, " "))
+ if subscription != None:
+ list.cp.set("list", "subscription", subscription)
+ if posting != None:
+ list.cp.set("list", "posting", posting)
+ if archived != None:
+ list.cp.set("list", "archived", archived)
+ if language != None:
+ list.cp.set("list", "language", language)
+ if ignore_bounce != None:
+ list.cp.set("list", "ignore-bounce", ignore_bounce)
+ if mail_on_sub_changes != None:
+ list.cp.set("list", "mail-on-subscription-changes",
+ mail_on_sub_changes)
+ if mail_on_forced_unsub != None:
+ list.cp.set("list", "mail-on-forced-unsubscribe",
+ mail_on_forced_unsub)
+
+
+def main(args):
+ try:
+ opts, args = getopt.getopt(args, "h",
+ ["name=",
+ "owner=",
+ "moderator=",
+ "subscription=",
+ "posting=",
+ "archived=",
+ "language=",
+ "ignore-bounce=",
+ "mail-on-forced-unsubscribe=",
+ "mail-on-subscription-changes=",
+ "skip-prefix=",
+ "domain=",
+ "sendmail=",
+ "smtp-server=",
+ "qmqp-server=",
+ "quiet",
+ "moderate",
+ "post",
+ "sender=",
+ "recipient=",
+ "no-act",
+
+ "set",
+ "get",
+ "help",
+ "create",
+ "destroy",
+ "subscribe",
+ "unsubscribe",
+ "list",
+ "is-list",
+ "edit",
+ "incoming",
+ "cleaning-woman",
+ "show-lists",
+ "version",
+ ])
+ except getopt.GetoptError, detail:
+ error("Error parsing command line options (see --help):\n%s" %
+ detail)
+
+ operation = None
+ list_name = None
+ owners = []
+ moderators = []
+ subscription = None
+ posting = None
+ archived = None
+ ignore_bounce = None
+ skip_prefix = None
+ domain = None
+ sendmail = "/usr/sbin/sendmail"
+ smtp_server = None
+ qmqp_server = None
+ moderate = 0
+ post = 0
+ sender = None
+ recipient = None
+ language = None
+ mail_on_forced_unsub = None
+ mail_on_sub_changes = None
+ no_act = 0
+ global quiet
+
+ for opt, arg in opts:
+ if opt == "--name":
+ list_name = arg
+ elif opt == "--owner":
+ owners.append(arg)
+ elif opt == "--moderator":
+ moderators.append(arg)
+ elif opt == "--subscription":
+ subscription = arg
+ elif opt == "--posting":
+ posting = arg
+ elif opt == "--archived":
+ archived = arg
+ elif opt == "--ignore-bounce":
+ ignore_bounce = arg
+ elif opt == "--skip-prefix":
+ skip_prefix = arg
+ elif opt == "--domain":
+ domain = arg
+ elif opt == "--sendmail":
+ sendmail = arg
+ elif opt == "--smtp-server":
+ smtp_server = arg
+ elif opt == "--qmqp-server":
+ qmqp_server = arg
+ elif opt == "--sender":
+ sender = arg
+ elif opt == "--recipient":
+ recipient = arg
+ elif opt == "--language":
+ language = arg
+ elif opt == "--mail-on-forced-unsubscribe":
+ mail_on_forced_unsub = arg
+ elif opt == "--mail-on-subscription-changes":
+ mail_on_sub_changes = arg
+ elif opt == "--moderate":
+ moderate = 1
+ elif opt == "--post":
+ post = 1
+ elif opt == "--quiet":
+ quiet = 1
+ elif opt == "--no-act":
+ no_act = 1
+ else:
+ operation = opt
+
+ if operation is None:
+ error("No operation specified, see --help.")
+
+ if list_name is None and operation not in ["--incoming", "--help", "-h",
+ "--cleaning-woman",
+ "--show-lists",
+ "--version"]:
+ error("%s requires a list name specified with --name" % operation)
+
+ if operation in ["--help", "-h"]:
+ usage()
+
+ if sender or recipient:
+ environ = os.environ.copy()
+ if sender:
+ environ["SENDER"] = sender
+ if recipient:
+ environ["RECIPIENT"] = recipient
+ set_environ(environ)
+
+ mlm = MailingListManager(DOTDIR, sendmail=sendmail,
+ smtp_server=smtp_server,
+ qmqp_server=qmqp_server)
+ if no_act:
+ mlm.send_mail = no_act_send_mail
+
+ if operation == "--create":
+ if not owners:
+ error("You must give at least one list owner with --owner.")
+ list = mlm.create_list(list_name)
+ set_list_options(list, owners, moderators, subscription, posting,
+ archived, language, ignore_bounce,
+ mail_on_sub_changes, mail_on_forced_unsub)
+ list.save_config()
+ debug("Created list %s." % list_name)
+ elif operation == "--destroy":
+ shutil.rmtree(os.path.join(DOTDIR, list_name))
+ debug("Removed list %s." % list_name)
+ elif operation == "--edit":
+ list = mlm.open_list(list_name)
+ set_list_options(list, owners, moderators, subscription, posting,
+ archived, language, ignore_bounce,
+ mail_on_sub_changes, mail_on_forced_unsub)
+ list.save_config()
+ elif operation == "--subscribe":
+ list = mlm.open_list(list_name)
+ list.subscribers.lock()
+ for address in args:
+ if address.find("@") == -1:
+ error("Address '%s' does not contain an @." % address)
+ list.subscribers.add(address)
+ debug("Added subscriber <%s>." % address)
+ list.subscribers.save()
+ elif operation == "--unsubscribe":
+ list = mlm.open_list(list_name)
+ list.subscribers.lock()
+ for address in args:
+ list.subscribers.remove(address)
+ debug("Removed subscriber <%s>." % address)
+ list.subscribers.save()
+ elif operation == "--list":
+ list = mlm.open_list(list_name)
+ for address in list.subscribers.get_all():
+ print address
+ elif operation == "--is-list":
+ if mlm.is_list(list_name, skip_prefix, domain):
+ debug("Indeed a mailing list: <%s>" % list_name)
+ else:
+ debug("Not a mailing list: <%s>" % list_name)
+ sys.exit(1)
+ elif operation == "--incoming":
+ mlm.incoming_message(skip_prefix, domain, moderate, post)
+ elif operation == "--cleaning-woman":
+ mlm.cleaning_woman()
+ elif operation == "--show-lists":
+ listnames = mlm.get_lists()
+ listnames.sort()
+ for listname in listnames:
+ print listname
+ elif operation == "--get":
+ list = mlm.open_list(list_name)
+ for name in args:
+ print list.cp.get("list", name)
+ elif operation == "--set":
+ list = mlm.open_list(list_name)
+ for arg in args:
+ if "=" not in arg:
+ error("Error: --set arguments must be of form name=value")
+ name, value = arg.split("=", 1)
+ list.cp.set("list", name, value)
+ list.save_config()
+ elif operation == "--version":
+ print "EoC, version %s" % VERSION
+ print "Home page: http://liw.iki.fi/liw/eoc/"
+ else:
+ error("Internal error: unimplemented option <%s>." % operation)
+
+if __name__ == "__main__":
+ try:
+ main(sys.argv[1:])
+ except EocException, detail:
+ error("Error: %s" % detail)