1 """Mailing list manager.
 
   3 This is a simple mailing list manager that mimicks the ezmlm-idx mail
 
   4 address commands. See manual page for more information.
 
   8 PLUGIN_INTERFACE_VERSION = "1"
 
  29 # The following values will be overriden by "make install".
 
  30 TEMPLATE_DIRS = ["./templates"]
 
  34 class EocException(Exception):
 
  36     def __init__(self, arg=None):
 
  42 class UnknownList(EocException):
 
  43     def __init__(self, list_name):
 
  44         self.msg = "%s is not a known mailing list" % list_name
 
  46 class BadCommandAddress(EocException):
 
  47     def __init__(self, address):
 
  48         self.msg = "%s is not a valid command address" % address
 
  50 class BadSignature(EocException):
 
  51     def __init__(self, address):
 
  52         self.msg = "address %s has an invalid digital signature" % address
 
  54 class ListExists(EocException):
 
  55     def __init__(self, list_name):
 
  56         self.msg = "Mailing list %s alreadys exists" % list_name
 
  58 class ListDoesNotExist(EocException):
 
  59     def __init__(self, list_name):
 
  60         self.msg = "Mailing list %s does not exist" % list_name
 
  62 class MissingEnvironmentVariable(EocException):
 
  63     def __init__(self, name):
 
  64         self.msg = "Environment variable %s does not exist" % name
 
  66 class MissingTemplate(EocException):
 
  67     def __init__(self, template):
 
  68         self.msg = "Template %s does not exit" % template
 
  71 # Names of commands EoC recognizes in e-mail addresses.
 
  72 SIMPLE_COMMANDS = ["help", "list", "owner", "setlist", "setlistsilently", "ignore"]
 
  73 SUB_COMMANDS = ["subscribe", "unsubscribe"]
 
  74 HASH_COMMANDS = ["subyes", "subapprove", "subreject", "unsubyes",
 
  75                  "bounce", "probe", "approve", "reject", "setlistyes",
 
  77 COMMANDS = SIMPLE_COMMANDS + SUB_COMMANDS + HASH_COMMANDS
 
  81     return md5.new(s).hexdigest()
 
  85 def set_environ(new_environ):
 
  89 def get_from_environ(key):
 
  96         return env[key].lower()
 
  97     raise MissingEnvironmentVariable(key)
 
 101     """A parser for incoming e-mail addresses."""
 
 103     def __init__(self, lists):
 
 104         self.set_lists(lists)
 
 105         self.set_skip_prefix(None)
 
 106         self.set_forced_domain(None)
 
 108     def set_lists(self, lists):
 
 109         """Set the list of canonical list names we should know about."""
 
 112     def set_skip_prefix(self, skip_prefix):
 
 113         """Set the prefix to be removed from an address."""
 
 114         self.skip_prefix = skip_prefix
 
 116     def set_forced_domain(self, forced_domain):
 
 117         """Set the domain part we should force the address to have."""
 
 118         self.forced_domain = forced_domain
 
 120     def clean(self, address):
 
 121         """Remove cruft from the address and convert the rest to lower case."""
 
 123             n = self.skip_prefix and len(self.skip_prefix)
 
 124             if address[:n] == self.skip_prefix:
 
 125                 address = address[n:]
 
 126         if self.forced_domain:
 
 127             parts = address.split("@", 1)
 
 128             address = "%s@%s" % (parts[0], self.forced_domain)
 
 129         return address.lower()
 
 131     def split_address(self, address):
 
 132         """Split an address to a local part and a domain."""
 
 133         parts = address.lower().split("@", 1)
 
 139     # Does an address refer to a list? If not, return None, else return a list
 
 140     # of additional parts (separated by hyphens) in the address. Note that []
 
 141     # is not the same as None.
 
 143     def additional_address_parts(self, address, listname):
 
 144         addr_local, addr_domain = self.split_address(address)
 
 145         list_local, list_domain = self.split_address(listname)
 
 147         if addr_domain != list_domain:
 
 150         if addr_local.lower() == list_local.lower():
 
 154         if addr_local[:n] != list_local or addr_local[n] != "-":
 
 157         return addr_local[n+1:].split("-")
 
 160     # Parse an address we have received that identifies a list we manage.
 
 161     # The address may contain command and signature parts. Return the name
 
 162     # of the list, and a sequence of the additional parts (split at hyphens).
 
 163     # Raise exceptions for errors. Note that the command will be valid, but
 
 164     # cryptographic signatures in the address is not checked.
 
 166     def parse(self, address):
 
 167         address = self.clean(address)
 
 168         for listname in self.lists:
 
 169             parts = self.additional_address_parts(address, listname)
 
 173                 return listname, parts
 
 174             elif parts[0] in HASH_COMMANDS:
 
 176                     raise BadCommandAddress(address)
 
 177                 return listname, parts
 
 178             elif parts[0] in COMMANDS:
 
 179                 return listname, parts
 
 181         raise UnknownList(address)
 
 184 class MailingListManager:
 
 186     def __init__(self, dotdir, sendmail="/usr/sbin/sendmail", lists=[],
 
 187                  smtp_server=None, qmqp_server=None):
 
 189         self.sendmail = sendmail
 
 190         self.smtp_server = smtp_server
 
 191         self.qmqp_server = qmqp_server
 
 194         self.secret = self.make_and_read_secret()
 
 197             lists = filter(lambda s: "@" in s, os.listdir(dotdir))
 
 198         self.set_lists(lists)
 
 200         self.simple_commands = ["help", "list", "owner", "setlist",
 
 201                                 "setlistsilently", "ignore"]
 
 202         self.sub_commands = ["subscribe", "unsubscribe"]
 
 203         self.hash_commands = ["subyes", "subapprove", "subreject", "unsubyes",
 
 204                               "bounce", "probe", "approve", "reject",
 
 205                               "setlistyes", "setlistsilentyes"]
 
 206         self.commands = self.simple_commands + self.sub_commands + \
 
 213     # Create the dot directory for us, if it doesn't exist already.
 
 214     def make_dotdir(self):
 
 215         if not os.path.isdir(self.dotdir):
 
 216             os.makedirs(self.dotdir, 0700)
 
 218     # Create the "secret" file, with a random value used as cookie for
 
 219     # verification addresses.
 
 220     def make_and_read_secret(self):
 
 221         secret_name = os.path.join(self.dotdir, "secret")
 
 222         if not os.path.isfile(secret_name):
 
 223             f = open("/dev/urandom", "r")
 
 226             f = open(secret_name, "w")
 
 230             f = open(secret_name, "r")
 
 235     # Load the plugins from DOTDIR/plugins/*.py.
 
 236     def load_plugins(self):
 
 239         dirname = os.path.join(DOTDIR, "plugins")
 
 241             plugins = os.listdir(dirname)
 
 246         plugins = map(os.path.splitext, plugins)
 
 247         plugins = filter(lambda p: p[1] == ".py", plugins)
 
 248         plugins = map(lambda p: p[0], plugins)
 
 250             pathname = os.path.join(dirname, name + ".py")
 
 251             f = open(pathname, "r")
 
 252             module = imp.load_module(name, f, pathname, 
 
 253                                      (".py", "r", imp.PY_SOURCE))
 
 255             if module.PLUGIN_INTERFACE_VERSION == PLUGIN_INTERFACE_VERSION:
 
 256                 self.plugins.append(module)
 
 258     # Call function named funcname (a string) in all plugins, giving as
 
 259     # arguments all the remaining arguments preceded by ml. Return value
 
 260     # of each function is the new list of arguments to the next function.
 
 261     # Return value of this function is the return value of the last function.
 
 262     def call_plugins(self, funcname, list, *args):
 
 263         for plugin in self.plugins:
 
 264             if plugin.__dict__.has_key(funcname):
 
 265                 args = apply(plugin.__dict__[funcname], (list,) + args)
 
 266                 if type(args) != type((0,)):
 
 270     # Set the list of listnames. The list of lists needs to be sorted in
 
 271     # length order so that test@example.com is matched before
 
 272     # test-list@example.com
 
 273     def set_lists(self, lists):
 
 274         temp = map(lambda s: (len(s), s), lists)
 
 276         self.lists = map(lambda t: t[1], temp)
 
 278     # Return the list of listnames.
 
 282     # Decode an address that has been encoded to be part of a local part.
 
 283     def decode_address(self, parts):
 
 284         return string.join(string.join(parts, "-").split("="), "@")
 
 286     # Is local_part@domain an existing list?
 
 287     def is_list_name(self, local_part, domain):
 
 288         return ("%s@%s" % (local_part, domain)) in self.lists
 
 290     # Compute the verification checksum for an address.
 
 291     def compute_hash(self, address):
 
 292         return md5sum_as_hex(address + self.secret)
 
 294     # Is the verification signature in a parsed address bad? If so, return true,
 
 295     # otherwise return false.
 
 296     def signature_is_bad(self, dict, hash):
 
 297         local_part, domain = dict["name"].split("@")
 
 298         address = "%s-%s-%s@%s" % (local_part, dict["command"], dict["id"], 
 
 300         correct = self.compute_hash(address)
 
 301         return correct != hash
 
 303     # Parse a command address we have received and check its validity
 
 304     # (including signature, if any). Return a dictionary with keys
 
 305     # "command", "sender" (address that was encoded into address, if
 
 306     # any), "id" (group ID).
 
 308     def parse_recipient_address(self, address, skip_prefix, forced_domain):
 
 309         ap = AddressParser(self.get_lists())
 
 310         ap.set_lists(self.get_lists())
 
 311         ap.set_skip_prefix(skip_prefix)
 
 312         ap.set_forced_domain(forced_domain)
 
 313         listname, parts = ap.parse(address)
 
 315         dict = { "name": listname }
 
 318             dict["command"] = "post"
 
 320             command, args = parts[0], parts[1:]
 
 321             dict["command"] = command
 
 322             if command in SUB_COMMANDS:
 
 323                 dict["sender"] = self.decode_address(args)
 
 324             elif command in HASH_COMMANDS:
 
 327                 if self.signature_is_bad(dict, hash):
 
 328                     raise BadSignature(address)
 
 332     # Does an address refer to a mailing list?
 
 333     def is_list(self, name, skip_prefix=None, domain=None):
 
 335             self.parse_recipient_address(name, skip_prefix, domain)
 
 336         except BadCommandAddress:
 
 344     # Create a new list and return it.
 
 345     def create_list(self, name):
 
 346         if self.is_list(name):
 
 347             raise ListExists(name)
 
 348         self.set_lists(self.lists + [name])
 
 349         return MailingList(self, name)
 
 351     # Open an existing list.
 
 352     def open_list(self, name):
 
 353         if self.is_list(name):
 
 354             return self.open_list_exact(name)
 
 357             for list in self.lists:
 
 358                 if list[:len(x)] == x:
 
 359                     return self.open_list_exact(list)
 
 360             raise ListDoesNotExist(name)
 
 362     def open_list_exact(self, name):
 
 363         for list in self.get_lists():
 
 364             if list.lower() == name.lower():
 
 365                 return MailingList(self, list)
 
 366         raise ListDoesNotExist(name)
 
 368     # Process an incoming message.
 
 369     def incoming_message(self, skip_prefix, domain, moderate, post):
 
 370         debug("Processing incoming message.")
 
 371         debug("$SENDER = <%s>" % get_from_environ("SENDER"))
 
 372         debug("$RECIPIENT = <%s>" % get_from_environ("RECIPIENT"))
 
 373         dict = self.parse_recipient_address(get_from_environ("RECIPIENT"),
 
 376         dict["force-moderation"] = moderate
 
 377         dict["force-posting"] = post
 
 378         debug("List is <%(name)s>, command is <%(command)s>." % dict)
 
 379         list = self.open_list_exact(dict["name"])
 
 382     # Clean up bouncing address and do other janitorial work for all lists.
 
 383     def cleaning_woman(self, send_mail=None):
 
 385         for listname in self.lists:
 
 386             list = self.open_list_exact(listname)
 
 388                 list.send_mail = send_mail
 
 389             list.cleaning_woman(now)
 
 391     # Send a mail to the desired recipients.
 
 392     def send_mail(self, envelope_sender, recipients, text):
 
 393         debug("send_mail:\n  sender=%s\n  recipients=%s\n  text=\n    %s" % 
 
 394               (envelope_sender, str(recipients), 
 
 395                "\n    ".join(text[:text.find("\n\n")].split("\n"))))
 
 399                     smtp = smtplib.SMTP(self.smtp_server)
 
 400                     smtp.sendmail(envelope_sender, recipients, text)
 
 403                     error("Error sending SMTP mail, mail probably not sent")
 
 405             elif self.qmqp_server:
 
 407                     q = qmqp.QMQP(self.qmqp_server)
 
 408                     q.sendmail(envelope_sender, recipients, text)
 
 411                     error("Error sending QMQP mail, mail probably not sent")
 
 414                 recipients = string.join(recipients, " ")
 
 415                 f = os.popen("%s -oi -f '%s' %s" % 
 
 423                     error("%s returned %d, mail sending probably failed" %
 
 424                            (self.sendmail, status))
 
 425                     sys.exit((status >> 8) & 0xff)
 
 427             debug("send_mail: no recipients, not sending")
 
 433     posting_opts = ["auto", "free", "moderated"]
 
 435     def __init__(self, mlm, name):
 
 439         self.cp = ConfigParser.ConfigParser()
 
 440         self.cp.add_section("list")
 
 441         self.cp.set("list", "owners", "")
 
 442         self.cp.set("list", "moderators", "")
 
 443         self.cp.set("list", "subscription", "free")
 
 444         self.cp.set("list", "posting", "free")
 
 445         self.cp.set("list", "archived", "no")
 
 446         self.cp.set("list", "mail-on-subscription-changes", "no")
 
 447         self.cp.set("list", "mail-on-forced-unsubscribe", "no")
 
 448         self.cp.set("list", "ignore-bounce", "no")
 
 449         self.cp.set("list", "language", "")
 
 450         self.cp.set("list", "pristine-headers", "")
 
 452         self.dirname = os.path.join(self.mlm.dotdir, name)
 
 454         self.cp.read(self.mkname("config"))
 
 456         self.subscribers = SubscriberDatabase(self.dirname, "subscribers")
 
 457         self.moderation_box = MessageBox(self.dirname, "moderation-box")
 
 458         self.subscription_box = MessageBox(self.dirname, "subscription-box")
 
 459         self.bounce_box = MessageBox(self.dirname, "bounce-box")
 
 461     def make_listdir(self):
 
 462         if not os.path.isdir(self.dirname):
 
 463             os.mkdir(self.dirname, 0700)
 
 465             f = open(self.mkname("subscribers"), "w")
 
 468     def mkname(self, relative):
 
 469         return os.path.join(self.dirname, relative)
 
 471     def save_config(self):
 
 472         f = open(self.mkname("config"), "w")
 
 476     def read_stdin(self):
 
 477         data = sys.stdin.read()
 
 478         # Skip Unix mbox "From " mail start indicator
 
 479         if data[:5] == "From ":
 
 480             data = string.split(data, "\n", 1)[1]
 
 483     def invent_boundary(self):
 
 484         return "%s/%s" % (md5sum_as_hex(str(time.time())),
 
 485                           md5sum_as_hex(self.name))
 
 487     def command_address(self, command):
 
 488         local_part, domain = self.name.split("@")
 
 489         return "%s-%s@%s" % (local_part, command, domain)
 
 491     def signed_address(self, command, id):
 
 492         unsigned = self.command_address("%s-%s" % (command, id))
 
 493         hash = self.mlm.compute_hash(unsigned)
 
 494         return self.command_address("%s-%s-%s" % (command, id, hash))
 
 497         return self.command_address("ignore")
 
 499     def nice_7bit(self, str):
 
 501             if (ord(c) < 32 and not c.isspace()) or ord(c) >= 127:
 
 505     def mime_encode_headers(self, text):
 
 507             headers, body = text.split("\n\n", 1)
 
 510             for line in headers.split("\n"):
 
 511                 if line[0].isspace():
 
 518                 if self.nice_7bit(header):
 
 519                     headers.append(header)
 
 522                         name, content = header.split(": ", 1)
 
 524                         name, content = header.split(":", 1)
 
 525                     hdr = email.Header.Header(content, "utf-8")
 
 526                     headers.append(name + ": " + hdr.encode())
 
 528             return "\n".join(headers) + "\n\n" + body
 
 530             error("Cannot MIME encode header, using original ones, sorry")
 
 533     def template(self, template_name, dict):
 
 534         lang = self.cp.get("list", "language")
 
 536             template_name_lang = template_name + "." + lang
 
 538             template_name_lang = template_name
 
 540         if not dict.has_key("list"):
 
 541             dict["list"] = self.name
 
 542             dict["local"], dict["domain"] = self.name.split("@")
 
 543         if not dict.has_key("list"):
 
 544             dict["list"] = self.name
 
 546         for dir in [os.path.join(self.dirname, "templates")] + TEMPLATE_DIRS:
 
 547             pathname = os.path.join(dir, template_name_lang)
 
 548             if not os.path.exists(pathname):
 
 549                 pathname = os.path.join(dir, template_name)
 
 550             if os.path.exists(pathname):
 
 551                 f = open(pathname, "r")
 
 556         raise MissingTemplate(template_name)
 
 558     def send_template(self, envelope_sender, sender, recipients,
 
 559                       template_name, dict):
 
 560         dict["From"] = "EoC <%s>" % sender
 
 561         dict["To"] = string.join(recipients, ", ")
 
 562         text = self.template(template_name, dict)
 
 565         if self.cp.get("list", "pristine-headers") != "yes":
 
 566             text = self.mime_encode_headers(text)
 
 567         self.mlm.send_mail(envelope_sender, recipients, text)
 
 569     def send_info_message(self, recipients, template_name, dict):
 
 570         self.send_template(self.command_address("ignore"),
 
 571                            self.command_address("help"),
 
 577         return self.cp.get("list", "owners").split()
 
 579     def moderators(self):
 
 580         return self.cp.get("list", "moderators").split()
 
 582     def is_list_owner(self, address):
 
 583         return address in self.owners()
 
 586         self.send_info_message([get_from_environ("SENDER")], "help", {})
 
 589         recipient = get_from_environ("SENDER")
 
 590         if self.is_list_owner(recipient):
 
 591             addr_list = self.subscribers.get_all()
 
 592             addr_text = string.join(addr_list, "\n")
 
 593             self.send_info_message([recipient], "list",
 
 595                                      "addresses": addr_text,
 
 596                                      "count": len(addr_list),
 
 599             self.send_info_message([recipient], "list-sorry", {})
 
 601     def obey_setlist(self, origmail):
 
 602         recipient = get_from_environ("SENDER")
 
 603         if self.is_list_owner(recipient):
 
 604             id = self.moderation_box.add(recipient, origmail)
 
 605             if self.parse_setlist_addresses(origmail) == None:
 
 606                 self.send_bad_addresses_in_setlist(id)
 
 607                 self.moderation_box.remove(id)
 
 609                 confirm = self.signed_address("setlistyes", id)
 
 610                 self.send_info_message(self.owners(), "setlist-confirm",
 
 613                                           "origmail": origmail,
 
 614                                           "boundary": self.invent_boundary(),
 
 618             self.send_info_message([recipient], "setlist-sorry", {})
 
 620     def obey_setlistsilently(self, origmail):
 
 621         recipient = get_from_environ("SENDER")
 
 622         if self.is_list_owner(recipient):
 
 623             id = self.moderation_box.add(recipient, origmail)
 
 624             if self.parse_setlist_addresses(origmail) == None:
 
 625                 self.send_bad_addresses_in_setlist(id)
 
 626                 self.moderation_box.remove(id)
 
 628                 confirm = self.signed_address("setlistsilentyes", id)
 
 629                 self.send_info_message(self.owners(), "setlist-confirm",
 
 632                                           "origmail": origmail,
 
 633                                           "boundary": self.invent_boundary(),
 
 636             self.info_message([recipient], "setlist-sorry", {})
 
 638     def parse_setlist_addresses(self, text):
 
 639         body = text.split("\n\n", 1)[1]
 
 640         lines = body.split("\n")
 
 641         lines = filter(lambda line: line != "", lines)
 
 642         badlines = filter(lambda line: "@" not in line, lines)
 
 648     def send_bad_addresses_in_setlist(self, id):
 
 649         addr = self.moderation_box.get_address(id)
 
 650         origmail = self.moderation_box.get(id)
 
 651         self.send_info_message([addr], "setlist-badlist",
 
 653                                 "origmail": origmail,
 
 654                                 "boundary": self.invent_boundary(),
 
 658     def obey_setlistyes(self, dict):
 
 659         if self.moderation_box.has(dict["id"]):
 
 660             text = self.moderation_box.get(dict["id"])
 
 661             addresses = self.parse_setlist_addresses(text)
 
 662             if addresses == None:
 
 663                 self.send_bad_addresses_in_setlist(id)
 
 665                 removed_subscribers = []
 
 666                 self.subscribers.lock()
 
 667                 old = self.subscribers.get_all()
 
 669                     if address.lower() not in map(string.lower, addresses):
 
 670                         self.subscribers.remove(address)
 
 671                         removed_subscribers.append(address)
 
 674                             if x.lower() == address.lower():
 
 676                 self.subscribers.add_many(addresses)
 
 677                 self.subscribers.save()
 
 679                 for recipient in addresses:
 
 680                     self.send_info_message([recipient], "sub-welcome", {})
 
 681                 for recipient in removed_subscribers:
 
 682                     self.send_info_message([recipient], "unsub-goodbye", {})
 
 683                 self.send_info_message(self.owners(), "setlist-done", {})
 
 685             self.moderation_box.remove(dict["id"])
 
 687     def obey_setlistsilentyes(self, dict):
 
 688         if self.moderation_box.has(dict["id"]):
 
 689             text = self.moderation_box.get(dict["id"])
 
 690             addresses = self.parse_setlist_addresses(text)
 
 691             if addresses == None:
 
 692                 self.send_bad_addresses_in_setlist(id)
 
 694                 self.subscribers.lock()
 
 695                 old = self.subscribers.get_all()
 
 697                     if address not in addresses:
 
 698                         self.subscribers.remove(address)
 
 700                         addresses.remove(address)
 
 701                 self.subscribers.add_many(addresses)
 
 702                 self.subscribers.save()
 
 703                 self.send_info_message(self.owners(), "setlist-done", {})
 
 705             self.moderation_box.remove(dict["id"])
 
 707     def obey_owner(self, text):
 
 708         sender = get_from_environ("SENDER")
 
 709         recipients = self.cp.get("list", "owners").split()
 
 710         self.mlm.send_mail(sender, recipients, text)
 
 712     def obey_subscribe_or_unsubscribe(self, dict, template_name, command, 
 
 715         requester  = get_from_environ("SENDER")
 
 716         subscriber = dict["sender"]
 
 718             subscriber = requester
 
 719         if subscriber.find("@") == -1:
 
 720             info("Trying to (un)subscribe address without @: %s" % subscriber)
 
 722         if self.cp.get("list", "ignore-bounce") == "yes":
 
 723             info("Will not (un)subscribe address: %s from static list" %subscriber)
 
 725         if requester in self.owners():
 
 726             confirmers = self.owners()
 
 728             confirmers = [subscriber]
 
 730         id = self.subscription_box.add(subscriber, origmail)
 
 731         confirm = self.signed_address(command, id)
 
 732         self.send_info_message(confirmers, template_name,
 
 735                                     "origmail": origmail,
 
 736                                     "boundary": self.invent_boundary(),
 
 739     def obey_subscribe(self, dict, origmail):
 
 740         self.obey_subscribe_or_unsubscribe(dict, "sub-confirm", "subyes", 
 
 743     def obey_unsubscribe(self, dict, origmail):
 
 744         self.obey_subscribe_or_unsubscribe(dict, "unsub-confirm", "unsubyes",
 
 747     def obey_subyes(self, dict):
 
 748         if self.subscription_box.has(dict["id"]):
 
 749             if self.cp.get("list", "subscription") == "free":
 
 750                 recipient = self.subscription_box.get_address(dict["id"])
 
 751                 self.subscribers.lock()
 
 752                 self.subscribers.add(recipient)
 
 753                 self.subscribers.save()
 
 754                 sender = self.command_address("help")
 
 755                 self.send_template(self.ignore(), sender, [recipient], 
 
 757                 self.subscription_box.remove(dict["id"])
 
 758                 if self.cp.get("list", "mail-on-subscription-changes")=="yes":
 
 759                     self.send_info_message(self.owners(), 
 
 760                                            "sub-owner-notification",
 
 762                                             "address": recipient,
 
 765                 recipients = self.cp.get("list", "owners").split()
 
 766                 confirm = self.signed_address("subapprove", dict["id"])
 
 767                 deny = self.signed_address("subreject", dict["id"])
 
 768                 subscriber = self.subscription_box.get_address(dict["id"])
 
 769                 origmail = self.subscription_box.get(dict["id"])
 
 770                 self.send_template(self.ignore(), deny, recipients, 
 
 775                                        "subscriber": subscriber,
 
 776                                        "origmail": origmail,
 
 777                                        "boundary": self.invent_boundary(),
 
 779                 recipient = self.subscription_box.get_address(dict["id"])
 
 780                 self.send_info_message([recipient], "sub-wait", {})
 
 782     def obey_subapprove(self, dict):
 
 783         if self.subscription_box.has(dict["id"]):
 
 784             recipient = self.subscription_box.get_address(dict["id"])
 
 785             self.subscribers.lock()
 
 786             self.subscribers.add(recipient)
 
 787             self.subscribers.save()
 
 788             self.send_info_message([recipient], "sub-welcome", {})
 
 789             self.subscription_box.remove(dict["id"])
 
 790             if self.cp.get("list", "mail-on-subscription-changes")=="yes":
 
 791                 self.send_info_message(self.owners(), "sub-owner-notification",
 
 793                                         "address": recipient,
 
 796     def obey_subreject(self, dict):
 
 797         if self.subscription_box.has(dict["id"]):
 
 798             recipient = self.subscription_box.get_address(dict["id"])
 
 799             self.send_info_message([recipient], "sub-reject", {})
 
 800             self.subscription_box.remove(dict["id"])
 
 802     def obey_unsubyes(self, dict):
 
 803         if self.subscription_box.has(dict["id"]):
 
 804             recipient = self.subscription_box.get_address(dict["id"])
 
 805             self.subscribers.lock()
 
 806             self.subscribers.remove(recipient)
 
 807             self.subscribers.save()
 
 808             self.send_info_message([recipient], "unsub-goodbye", {})
 
 809             self.subscription_box.remove(dict["id"])
 
 810             if self.cp.get("list", "mail-on-subscription-changes")=="yes":
 
 811                 self.send_info_message(self.owners(),
 
 812                                        "unsub-owner-notification",
 
 814                                         "address": recipient,
 
 817     def store_into_archive(self, text):
 
 818         if self.cp.get("list", "archived") == "yes":
 
 819             archdir = os.path.join(self.dirname, "archive")
 
 820             if not os.path.exists(archdir):
 
 821                 os.mkdir(archdir, 0700)
 
 822             id = md5sum_as_hex(text)
 
 823             f = open(os.path.join(archdir, id), "w")
 
 827     def list_headers(self):
 
 828         local, domain = self.name.split("@")
 
 830         list.append("List-Id: <%s.%s>" % (local, domain))
 
 831         list.append("List-Help: <mailto:%s-help@%s>" % (local, domain))
 
 832         list.append("List-Unsubscribe: <mailto:%s-unsubscribe@%s>" % 
 
 834         list.append("List-Subscribe: <mailto:%s-subscribe@%s>" % 
 
 836         list.append("List-Post: <mailto:%s@%s>" % (local, domain))
 
 837         list.append("List-Owner: <mailto:%s-owner@%s>" % (local, domain))
 
 838         list.append("Precedence: bulk");
 
 839         return string.join(list, "\n") + "\n"
 
 841     def read_file(self, basename):
 
 843             f = open(os.path.join(self.dirname, basename), "r")
 
 850     def headers_to_add(self):
 
 851         headers_to_add = self.read_file("headers-to-add").rstrip()
 
 853             return headers_to_add + "\n"
 
 857     def remove_some_headers(self, mail, headers_to_remove):
 
 858         endpos = mail.find("\n\n")
 
 860             endpos = mail.find("\n\r\n")
 
 863         headers = mail[:endpos].split("\n")
 
 866         headers_to_remove = [x.lower() for x in headers_to_remove]
 
 869         add_continuation_lines = 0
 
 871         for header in headers:
 
 872             if header[0] in [' ','\t']:
 
 873                 # this is a continuation line
 
 874                 if add_continuation_lines:
 
 875                     remaining.append(header)
 
 877                 pos = header.find(":")
 
 879                     # malformed message, try to remove the junk
 
 880                     add_continuation_lines = 0
 
 882                 name = header[:pos].lower()
 
 883                 if name in headers_to_remove:
 
 884                     add_continuation_lines = 0
 
 886                     add_continuation_lines = 1
 
 887                     remaining.append(header)
 
 889         return "\n".join(remaining) + body
 
 891     def headers_to_remove(self, text):
 
 892         headers_to_remove = self.read_file("headers-to-remove").split("\n")
 
 893         headers_to_remove = map(lambda s: s.strip().lower(), 
 
 895         return self.remove_some_headers(text, headers_to_remove)
 
 897     def append_footer(self, text):
 
 898         if "base64" in text or "BASE64" in text:
 
 900             for line in StringIO.StringIO(text):
 
 901                 if line.lower().startswith("content-transfer-encoding:") and \
 
 902                    "base64" in line.lower():
 
 904         return text + self.template("footer", {})
 
 906     def send_mail_to_subscribers(self, text):
 
 907         text = self.remove_some_headers(text, ["list-id", "list-help",
 
 909                                                "list-subscribe", "list-post",
 
 910                                                "list-owner", "precedence"])
 
 911         text = self.headers_to_add() + self.list_headers() + \
 
 912                self.headers_to_remove(text)
 
 913         text = self.append_footer(text)
 
 914         text, = self.mlm.call_plugins("send_mail_to_subscribers_hook",
 
 916         if have_email_module and \
 
 917            self.cp.get("list", "pristine-headers") != "yes":
 
 918             text = self.mime_encode_headers(text)
 
 919         self.store_into_archive(text)
 
 920         for group in self.subscribers.groups():
 
 921             bounce = self.signed_address("bounce", group)
 
 922             addresses = self.subscribers.in_group(group)
 
 923             self.mlm.send_mail(bounce, addresses, text)
 
 925     def post_into_moderate(self, poster, dict, text):
 
 926         id = self.moderation_box.add(poster, text)
 
 927         recipients = self.moderators()
 
 929             recipients = self.owners()
 
 931         confirm = self.signed_address("approve", id)
 
 932         deny = self.signed_address("reject", id)
 
 933         self.send_template(self.ignore(), deny, recipients, "msg-moderate",
 
 938                             "boundary": self.invent_boundary(),
 
 940         self.send_info_message([poster], "msg-wait", {})
 
 942     def should_be_moderated(self, posting, poster):
 
 943         if posting == "moderated":
 
 945         if posting == "auto":
 
 946             if poster.lower() not in \
 
 947                 map(string.lower, self.subscribers.get_all()):
 
 951     def obey_post(self, dict, text):
 
 952         if dict.has_key("force-moderation") and dict["force-moderation"]:
 
 956         if dict.has_key("force-posting") and dict["force-posting"]:
 
 960         posting = self.cp.get("list", "posting")
 
 961         if posting not in self.posting_opts:
 
 962             error("You have a weird 'posting' config. Please, review it")
 
 963         poster = get_from_environ("SENDER")
 
 965             self.post_into_moderate(poster, dict, text)
 
 967             self.send_mail_to_subscribers(text)
 
 968         elif self.should_be_moderated(posting, poster):
 
 969             self.post_into_moderate(poster, dict, text)
 
 971             self.send_mail_to_subscribers(text)
 
 973     def obey_approve(self, dict):
 
 974         if self.moderation_box.lock(dict["id"]):
 
 975             if self.moderation_box.has(dict["id"]):
 
 976                 text = self.moderation_box.get(dict["id"])
 
 977                 self.send_mail_to_subscribers(text)
 
 978                 self.moderation_box.remove(dict["id"])
 
 979             self.moderation_box.unlock(dict["id"])
 
 981     def obey_reject(self, dict):
 
 982         if self.moderation_box.lock(dict["id"]):
 
 983             if self.moderation_box.has(dict["id"]):
 
 984                 self.moderation_box.remove(dict["id"])
 
 985             self.moderation_box.unlock(dict["id"])
 
 987     def split_address_list(self, addrs):
 
 990             userpart, domain = addr.split("@")
 
 991             if domains.has_key(domain):
 
 992                 domains[domain].append(addr)
 
 994                 domains[domain] = [addr]
 
 996         if len(domains.keys()) == 1:
 
 998                 result.append([addr])
 
1000             result = domains.values()
 
1003     def obey_bounce(self, dict, text):
 
1004         if self.subscribers.has_group(dict["id"]):
 
1005             self.subscribers.lock()
 
1006             addrs = self.subscribers.in_group(dict["id"])
 
1008                 if self.cp.get("list", "ignore-bounce") == "yes":
 
1009                     info("Address <%s> bounced, ignoring bounce as configured." %
 
1011                     self.subscribers.unlock()
 
1013                 debug("Address <%s> bounced, setting state to bounce." %
 
1015                 bounce_id = self.bounce_box.add(addrs[0], text[:4096])
 
1016                 self.subscribers.set(dict["id"], "status", "bounced")
 
1017                 self.subscribers.set(dict["id"], "timestamp-bounced", 
 
1019                 self.subscribers.set(dict["id"], "bounce-id",
 
1022                 debug("Group %s bounced, splitting." % dict["id"])
 
1023                 for new_addrs in self.split_address_list(addrs):
 
1024                     self.subscribers.add_many(new_addrs)
 
1025                 self.subscribers.remove_group(dict["id"])
 
1026             self.subscribers.save()
 
1028             debug("Ignoring bounce, group %s doesn't exist (anymore?)." %
 
1031     def obey_probe(self, dict, text):
 
1033         if self.subscribers.has_group(id):
 
1034             self.subscribers.lock()
 
1035             if self.subscribers.get(id, "status") == "probed":
 
1036                 self.subscribers.set(id, "status", "probebounced")
 
1037             self.subscribers.save()
 
1039     def obey(self, dict):
 
1040         text = self.read_stdin()
 
1042         if dict["command"] in ["help", "list", "subscribe", "unsubscribe",
 
1043                                "subyes", "subapprove", "subreject",
 
1044                                "unsubyes", "post", "approve"]:
 
1045             sender = get_from_environ("SENDER")
 
1047                 debug("Ignoring bounce message for %s command." % 
 
1051         if dict["command"] == "help":
 
1053         elif dict["command"] == "list":
 
1055         elif dict["command"] == "owner":
 
1056             self.obey_owner(text)
 
1057         elif dict["command"] == "subscribe":
 
1058             self.obey_subscribe(dict, text)
 
1059         elif dict["command"] == "unsubscribe":
 
1060             self.obey_unsubscribe(dict, text)
 
1061         elif dict["command"] == "subyes":
 
1062             self.obey_subyes(dict)
 
1063         elif dict["command"] == "subapprove":
 
1064             self.obey_subapprove(dict)
 
1065         elif dict["command"] == "subreject":
 
1066             self.obey_subreject(dict)
 
1067         elif dict["command"] == "unsubyes":
 
1068             self.obey_unsubyes(dict)
 
1069         elif dict["command"] == "post":
 
1070             self.obey_post(dict, text)
 
1071         elif dict["command"] == "approve":
 
1072             self.obey_approve(dict)
 
1073         elif dict["command"] == "reject":
 
1074             self.obey_reject(dict)
 
1075         elif dict["command"] == "bounce":
 
1076             self.obey_bounce(dict, text)
 
1077         elif dict["command"] == "probe":
 
1078             self.obey_probe(dict, text)
 
1079         elif dict["command"] == "setlist":
 
1080             self.obey_setlist(text)
 
1081         elif dict["command"] == "setlistsilently":
 
1082             self.obey_setlistsilently(text)
 
1083         elif dict["command"] == "setlistyes":
 
1084             self.obey_setlistyes(dict)
 
1085         elif dict["command"] == "setlistsilentyes":
 
1086             self.obey_setlistsilentyes(dict)
 
1087         elif dict["command"] == "ignore":
 
1090     def get_bounce_text(self, id):
 
1091         bounce_id = self.subscribers.get(id, "bounce-id")
 
1092         if self.bounce_box.has(bounce_id):
 
1093             bounce_text = self.bounce_box.get(bounce_id)
 
1094             bounce_text = string.join(map(lambda s: "> " + s + "\n",
 
1095                                           bounce_text.split("\n")), "")
 
1097             bounce_text = "Bounce message not available."
 
1100     one_week = 7.0 * 24.0 * 60.0 * 60.0
 
1102     def handle_bounced_groups(self, now):
 
1103         for id in self.subscribers.groups():
 
1104             status = self.subscribers.get(id, "status") 
 
1105             t = float(self.subscribers.get(id, "timestamp-bounced")) 
 
1106             if status == "bounced":
 
1107                 if now - t > self.one_week:
 
1108                     sender = self.signed_address("probe", id) 
 
1109                     recipients = self.subscribers.in_group(id) 
 
1110                     self.send_template(sender, sender, recipients,
 
1112                                         "bounce": self.get_bounce_text(id),
 
1113                                         "boundary": self.invent_boundary(),
 
1115                     self.subscribers.set(id, "status", "probed")
 
1116             elif status == "probed":
 
1117                 if now - t > 2 * self.one_week:
 
1118                     debug(("Cleaning woman: probe didn't bounce " + 
 
1119                           "for group <%s>, setting status to ok.") % id)
 
1120                     self.subscribers.set(id, "status", "ok")
 
1121                     self.bounce_box.remove(
 
1122                             self.subscribers.get(id, "bounce-id"))
 
1123             elif status == "probebounced":
 
1124                 sender = self.command_address("help") 
 
1125                 for address in self.subscribers.in_group(id):
 
1126                     if self.cp.get("list", "mail-on-forced-unsubscribe") \
 
1128                         self.send_template(sender, sender,
 
1130                                        "bounce-owner-notification",
 
1133                                         "bounce": self.get_bounce_text(id),
 
1134                                         "boundary": self.invent_boundary(),
 
1137                     self.bounce_box.remove(
 
1138                             self.subscribers.get(id, "bounce-id"))
 
1139                     self.subscribers.remove(address) 
 
1140                     debug("Cleaning woman: removing <%s>." % address)
 
1141                     self.send_template(sender, sender, [address],
 
1142                                        "bounce-goodbye", {})
 
1144     def join_nonbouncing_groups(self, now):
 
1146         for id in self.subscribers.groups():
 
1147             status = self.subscribers.get(id, "status")
 
1148             age1 = now - float(self.subscribers.get(id, "timestamp-bounced"))
 
1149             age2 = now - float(self.subscribers.get(id, "timestamp-created"))
 
1151                 if age1 > self.one_week and age2 > self.one_week:
 
1152                     to_be_joined.append(id)
 
1155             for id in to_be_joined:
 
1156                 addrs = addrs + self.subscribers.in_group(id)
 
1157             self.subscribers.add_many(addrs)
 
1158             for id in to_be_joined:
 
1159                 self.bounce_box.remove(self.subscribers.get(id, "bounce-id"))
 
1160                 self.subscribers.remove_group(id)
 
1162     def remove_empty_groups(self):
 
1163         for id in self.subscribers.groups()[:]:
 
1164             if len(self.subscribers.in_group(id)) == 0:
 
1165                 self.subscribers.remove_group(id)
 
1167     def cleaning_woman(self, now):
 
1168         if self.subscribers.lock():
 
1169             self.handle_bounced_groups(now)
 
1170             self.join_nonbouncing_groups(now)
 
1171             self.subscribers.save()
 
1173 class SubscriberDatabase:
 
1175     def __init__(self, dirname, name):
 
1177         self.filename = os.path.join(dirname, name)
 
1178         self.lockname = os.path.join(dirname, "lock")
 
1183         if os.system("lockfile -l 60 %s" % self.lockname) == 0:
 
1189         os.remove(self.lockname)
 
1193         if not self.loaded and not self.dict:
 
1194             f = open(self.filename, "r")
 
1195             for line in f.xreadlines():
 
1196                 parts = line.split()
 
1197                 self.dict[parts[0]] = {
 
1199                     "timestamp-created": parts[2],
 
1200                     "timestamp-bounced": parts[3],
 
1201                     "bounce-id": parts[4],
 
1202                     "addresses": parts[5:],
 
1210         f = open(self.filename + ".new", "w")
 
1211         for id in self.dict.keys():
 
1213             f.write("%s " % self.dict[id]["status"])
 
1214             f.write("%s " % self.dict[id]["timestamp-created"])
 
1215             f.write("%s " % self.dict[id]["timestamp-bounced"])
 
1216             f.write("%s " % self.dict[id]["bounce-id"])
 
1217             f.write("%s\n" % string.join(self.dict[id]["addresses"], " "))
 
1219         os.remove(self.filename)
 
1220         os.rename(self.filename + ".new", self.filename)
 
1223     def get(self, id, attribute):
 
1225         if self.dict.has_key(id) and self.dict[id].has_key(attribute):
 
1226             return self.dict[id][attribute]
 
1229     def set(self, id, attribute, value):
 
1232         if self.dict.has_key(id) and self.dict[id].has_key(attribute):
 
1233             self.dict[id][attribute] = value
 
1235     def add(self, address):
 
1236         return self.add_many([address])
 
1238     def add_many(self, addresses):
 
1241         for addr in addresses[:]:
 
1242             if addr.find("@") == -1:
 
1243                 info("Address '%s' does not contain an @, ignoring it." % addr)
 
1244                 addresses.remove(addr)
 
1245         for id in self.dict.keys():
 
1246             old_ones = self.dict[id]["addresses"]
 
1247             for addr in addresses:
 
1249                     if x.lower() == addr.lower():
 
1251             self.dict[id]["addresses"] = old_ones
 
1252         id = self.new_group()
 
1255             "timestamp-created": self.timestamp(),
 
1256             "timestamp-bounced": "0",
 
1257             "bounce-id": "..notexist..",
 
1258             "addresses": addresses,
 
1262     def new_group(self):
 
1263         keys = self.dict.keys()
 
1265             keys = map(lambda x: int(x), keys)
 
1267             return "%d" % (keys[-1] + 1)
 
1271     def timestamp(self):
 
1272         return "%.0f" % time.time()
 
1277         for values in self.dict.values():
 
1278             list = list + values["addresses"]
 
1283         return self.dict.keys()
 
1285     def has_group(self, id):
 
1287         return self.dict.has_key(id)
 
1289     def in_group(self, id):
 
1291         return self.dict[id]["addresses"]
 
1293     def remove(self, address):
 
1296         for id in self.dict.keys():
 
1297             group = self.dict[id]
 
1298             for x in group["addresses"][:]:
 
1299                 if x.lower() == address.lower():
 
1300                     group["addresses"].remove(x)
 
1301                     if len(group["addresses"]) == 0:
 
1304     def remove_group(self, id):
 
1312     def __init__(self, dirname, boxname):
 
1313         self.boxdir = os.path.join(dirname, boxname)
 
1314         if not os.path.isdir(self.boxdir):
 
1315             os.mkdir(self.boxdir, 0700)
 
1317     def filename(self, id):
 
1318         return os.path.join(self.boxdir, id)
 
1320     def add(self, address, message_text):
 
1321         id = self.make_id(message_text)
 
1322         filename = self.filename(id)
 
1323         f = open(filename + ".address", "w")
 
1326         f = open(filename + ".new", "w")
 
1327         f.write(message_text)
 
1329         os.rename(filename + ".new", filename)
 
1332     def make_id(self, message_text):
 
1333         return md5sum_as_hex(message_text)
 
1334         # XXX this might be unnecessarily long
 
1336     def remove(self, id):
 
1337         filename = self.filename(id)
 
1338         if os.path.isfile(filename):
 
1340             os.remove(filename + ".address")
 
1343         return os.path.isfile(self.filename(id))
 
1345     def get_address(self, id):
 
1346         f = open(self.filename(id) + ".address", "r")
 
1352         f = open(self.filename(id), "r")
 
1357     def lockname(self, id):
 
1358         return self.filename(id) + ".lock"
 
1361         if os.system("lockfile -l 600 %s" % self.lockname(id)) == 0:
 
1366     def unlock(self, id):
 
1368             os.remove(self.lockname(id))
 
1376     def write(self, str):
 
1380 log_file_handle = None
 
1382     global log_file_handle
 
1383     if log_file_handle is None:
 
1385             log_file_handle = open(os.path.join(DOTDIR, "logfile.txt"), "a")
 
1387             log_file_handle = DevNull()
 
1388     return log_file_handle
 
1391     tuple = time.localtime(time.time())
 
1392     return time.strftime("%Y-%m-%d %H:%M:%S", tuple) + " [%d]" % os.getpid()
 
1398 # No logging to stderr of debug messages. Some MTAs have a limit on how
 
1399 # much data they accept via stderr and debug logs will fill that quickly.
 
1401     log_file().write(timestamp() + " " + msg + "\n")
 
1404 # Log to log file first, in case MTA's stderr buffer fills up and we lose
 
1407     log_file().write(timestamp() + " " + msg + "\n")
 
1408     sys.stderr.write(msg + "\n")
 
1417     sys.stdout.write("""\
 
1418 Usage: enemies-of-carlotta [options] command
 
1419 Mailing list manager.
 
1422   --name=listname@domain
 
1423   --owner=address@domain
 
1424   --moderator=address@domain
 
1425   --subscription=free/moderated
 
1426   --posting=free/moderated/auto
 
1428   --ignore-bounce=yes/no
 
1429   --language=language code or empty
 
1430   --mail-on-forced-unsubscribe=yes/no
 
1431   --mail-on-subscription-changes=yes/no
 
1432   --skip-prefix=string
 
1433   --domain=domain.name
 
1434   --smtp-server=domain.name
 
1450 For more detailed information, please read the enemies-of-carlotta(1)
 
1456 def no_act_send_mail(sender, recipients, text):
 
1457     print "NOT SENDING MAIL FOR REAL!"
 
1458     print "Sender:", sender
 
1459     print "Recipients:", recipients
 
1461     print "\n".join(map(lambda s: "   " + s, text.split("\n")))
 
1464 def set_list_options(list, owners, moderators, subscription, posting, 
 
1465                      archived, language, ignore_bounce,
 
1466                      mail_on_sub_changes, mail_on_forced_unsub):
 
1468         list.cp.set("list", "owners", string.join(owners, " "))
 
1470         list.cp.set("list", "moderators", string.join(moderators, " "))
 
1471     if subscription != None:
 
1472         list.cp.set("list", "subscription", subscription)
 
1474         list.cp.set("list", "posting", posting)
 
1475     if archived != None:
 
1476         list.cp.set("list", "archived", archived)
 
1477     if language != None:
 
1478         list.cp.set("list", "language", language)
 
1479     if ignore_bounce != None:
 
1480         list.cp.set("list", "ignore-bounce", ignore_bounce)
 
1481     if mail_on_sub_changes != None:
 
1482         list.cp.set("list", "mail-on-subscription-changes", 
 
1483                             mail_on_sub_changes)
 
1484     if mail_on_forced_unsub != None:
 
1485         list.cp.set("list", "mail-on-forced-unsubscribe",
 
1486                             mail_on_forced_unsub)
 
1491         opts, args = getopt.getopt(args, "h",
 
1500                                     "mail-on-forced-unsubscribe=",
 
1501                                     "mail-on-subscription-changes=",
 
1529     except getopt.GetoptError, detail:
 
1530         error("Error parsing command line options (see --help):\n%s" % 
 
1540     ignore_bounce = None
 
1543     sendmail = "/usr/sbin/sendmail"
 
1551     mail_on_forced_unsub = None
 
1552     mail_on_sub_changes = None
 
1556     for opt, arg in opts:
 
1559         elif opt == "--owner":
 
1561         elif opt == "--moderator":
 
1562             moderators.append(arg)
 
1563         elif opt == "--subscription":
 
1565         elif opt == "--posting":
 
1567         elif opt == "--archived":
 
1569         elif opt == "--ignore-bounce":
 
1571         elif opt == "--skip-prefix":
 
1573         elif opt == "--domain":
 
1575         elif opt == "--sendmail":
 
1577         elif opt == "--smtp-server":
 
1579         elif opt == "--qmqp-server":
 
1581         elif opt == "--sender":
 
1583         elif opt == "--recipient":
 
1585         elif opt == "--language":
 
1587         elif opt == "--mail-on-forced-unsubscribe":
 
1588             mail_on_forced_unsub = arg
 
1589         elif opt == "--mail-on-subscription-changes":
 
1590             mail_on_sub_changes = arg
 
1591         elif opt == "--moderate":
 
1593         elif opt == "--post":
 
1595         elif opt == "--quiet":
 
1597         elif opt == "--no-act":
 
1602     if operation is None:
 
1603         error("No operation specified, see --help.")
 
1605     if list_name is None and operation not in ["--incoming", "--help", "-h",
 
1609         error("%s requires a list name specified with --name" % operation)
 
1611     if operation in ["--help", "-h"]:
 
1614     if sender or recipient:
 
1615         environ = os.environ.copy()
 
1617             environ["SENDER"] = sender
 
1619             environ["RECIPIENT"] = recipient
 
1620         set_environ(environ)
 
1622     mlm = MailingListManager(DOTDIR, sendmail=sendmail, 
 
1623                              smtp_server=smtp_server,
 
1624                              qmqp_server=qmqp_server)
 
1626         mlm.send_mail = no_act_send_mail
 
1628     if operation == "--create":
 
1630             error("You must give at least one list owner with --owner.")
 
1631         list = mlm.create_list(list_name)
 
1632         set_list_options(list, owners, moderators, subscription, posting, 
 
1633                          archived, language, ignore_bounce,
 
1634                          mail_on_sub_changes, mail_on_forced_unsub)
 
1636         debug("Created list %s." % list_name)
 
1637     elif operation == "--destroy":
 
1638         shutil.rmtree(os.path.join(DOTDIR, list_name))
 
1639         debug("Removed list %s." % list_name)
 
1640     elif operation == "--edit":
 
1641         list = mlm.open_list(list_name)
 
1642         set_list_options(list, owners, moderators, subscription, posting, 
 
1643                          archived, language, ignore_bounce,
 
1644                          mail_on_sub_changes, mail_on_forced_unsub)
 
1646     elif operation == "--subscribe":
 
1647         list = mlm.open_list(list_name)
 
1648         list.subscribers.lock()
 
1649         for address in args:
 
1650             if address.find("@") == -1:
 
1651                 error("Address '%s' does not contain an @." % address)
 
1652             list.subscribers.add(address)
 
1653             debug("Added subscriber <%s>." % address)
 
1654         list.subscribers.save()
 
1655     elif operation == "--unsubscribe":
 
1656         list = mlm.open_list(list_name)
 
1657         list.subscribers.lock()
 
1658         for address in args:
 
1659             list.subscribers.remove(address)
 
1660             debug("Removed subscriber <%s>." % address)
 
1661         list.subscribers.save()
 
1662     elif operation == "--list":
 
1663         list = mlm.open_list(list_name)
 
1664         for address in list.subscribers.get_all():
 
1666     elif operation == "--is-list":
 
1667         if mlm.is_list(list_name, skip_prefix, domain):
 
1668             debug("Indeed a mailing list: <%s>" % list_name)
 
1670             debug("Not a mailing list: <%s>" % list_name)
 
1672     elif operation == "--incoming":
 
1673         mlm.incoming_message(skip_prefix, domain, moderate, post)
 
1674     elif operation == "--cleaning-woman":
 
1675         mlm.cleaning_woman()
 
1676     elif operation == "--show-lists":
 
1677         listnames = mlm.get_lists()
 
1679         for listname in listnames:
 
1681     elif operation == "--get":
 
1682         list = mlm.open_list(list_name)
 
1684             print list.cp.get("list", name)
 
1685     elif operation == "--set":
 
1686         list = mlm.open_list(list_name)
 
1689                 error("Error: --set arguments must be of form name=value")
 
1690             name, value = arg.split("=", 1)
 
1691             list.cp.set("list", name, value)
 
1693     elif operation == "--version":
 
1694         print "EoC, version %s" % VERSION
 
1695         print "Home page: http://liw.iki.fi/liw/eoc/"
 
1697         error("Internal error: unimplemented option <%s>." % operation)
 
1699 if __name__ == "__main__":
 
1702     except EocException, detail:
 
1703         error("Error: %s" % detail)