#!/usr/bin/perl -w
# majordomo2mailman.pl - Migrate Majordomo mailing lists to Mailman 2.0
# Copyright (C) 2002 Heiko Rommel (rommel@suse.de)
# BAW: Note this probably needs to be upgraded to work with MM2.1
#
# License:
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 1, or (at your option)
# any later version.
#
# Warranty:
#
# There's absolutely no warranty.
#
# comments on possible debug messages during the conversion:
#
# "not an valid email address" : those addresses are rejected, i.e. not imported into the Mailman list
# "not a numeric value" : such a value will be converted to 0 (z.B. maxlength)
# "already subscribed" : will only once be subscribed on the Mailman list
# "...umbrella..." or "...taboo..." -> Mailman-Admin-Guide
use strict;
use Getopt::Long;
use Fcntl;
use POSIX qw (tmpnam);
use vars qw (
$majordomo $mydomain $myurl
$aliasin $listdir
$aliasout $mailmanbin
$umbrella_member_suffix $private
$newsserver $newsprefix
$susehack $susearchuser
$help $debug $update $all $usagemsg
*FH
%mlaliases %mlowners %mlapprovers
%defaultmlconf %mlconf
%defaultmmconf %mmconf
);
#
# adjust your site-specific settings here
#
$mydomain = "my.domain";
$majordomo = "majordomo"; # the master Majordomo address for your site
$aliasin = "/var/lib/majordomo/aliases";
$listdir = "/var/lib/majordomo/lists";
$aliasout = "/tmp/aliases";
$myurl = "http://my.domain/mailman/";
$mailmanbin = "/usr/lib/mailman/bin";
$umbrella_member_suffix = "-owner";
$private = "yes"; # is this a private/Intranet site ?
$newsserver = "news.my.domain";
$newsprefix = "intern.";
$susehack = "no";
$susearchuser = "archdummy";
#
# 0)
# parse the command line arguments
#
$usagemsg = "usage: majordomo2mailman [-h|--help] [-d|--debug] [-u|--update] < (-a|--all) | list-of-mailinglists >";
GetOptions(
"h|help" => \$help,
"d|debug" => \$debug,
"a|all" => \$all,
"u|update" => \$update
) or die "$usagemsg\n";
if (defined($help)) { die "$usagemsg\n"; }
if ((not defined($all)) and (@ARGV<1)) { die "$usagemsg\n"; }
if ($<) { die "this script must be run as root!\n"; }
#
# 1)
# build a list of all aliases and extract the name of mailing lists plus their owners
#
%mlaliases = %mlowners = %mlapprovers = ();
open (FH, "< $aliasin") or die "can't open $aliasin\n";
while (<FH>) {
# first, build a list of all active aliases and their resolution
if (/^([^\#:]+)\s*:\s*(.*)$/) {
$mlaliases{$1} = $2;
}
}
my $mlalias;
for $mlalias (keys %mlaliases) {
# if we encounter an alias with :include: as expansion
# it is save to assume that the alias has the form
# <mailinglist>-outgoing -
# that way we find the names of all active mailing lists
if ($mlaliases{$mlalias} =~ /\:include\:/) {
my $ml;
($ml = $mlalias) =~ s/-outgoing//g;
$mlowners{$ml} = $mlaliases{"owner-$ml"};
$mlapprovers{$ml} = $mlaliases{"$ml-approval"};
}
}
close (FH);
#
# 2)
# for each list read the Majordomo configuration params
# and create a Mailman clone
#
my $ml;
for $ml ((defined ($all)) ? sort keys %mlowners : @ARGV) {
init_defaultmlconf($ml);
%mlconf = %defaultmlconf;
init_defaultmmconf($ml);
%mmconf = %defaultmmconf;
my @privileged; # addresses that are mentioned in restrict_post
my @members;
my ($primaryowner, @secondaryowner);
my ($primaryapprover, @secondaryapprover);
my ($skey, $terminator);
my $filename;
my @args;
#
# a)
# parse the configuration file
#
open (FH, "< $listdir/$ml.config") or die "can't open $listdir/$ml.config\n";
while (<FH>) {
# key = value ?
if (/^\s*([^=\#\s]+)\s*=\s*(.*)\s*$/) {
$mlconf{$1} = $2;
}
# key << EOF
# value
# EOF ?
elsif (/^\s*([^<\#\s]+)\s*<<\s*(.*)\s*$/) {
($skey, $terminator) = ($1, $2);
while (<FH>) {
last if (/^$terminator\s*$/);
$mlconf{$skey} .= $_;
}
chomp $mlconf{$skey};
}
}
close (FH);
#
# b)
# test if there are so-called flag files (clue that this is an old-style Majordomo lists)
# and overwrite previously parsed values
# (stolen from majordomo::config_parse.pl: handle_flag_files())
#
if ( -e "$listdir/$ml.private") {
$mlconf{"get_access"} = "closed";
$mlconf{"index_access"} = "closed";
$mlconf{"who_access"} = "closed";
$mlconf{"which_access"} = "closed";
}
$mlconf{"subscribe_policy"} = "closed" if ( -e "$listdir/$ml.closed");
$mlconf{"unsubscribe_policy"} = "closed" if ( -e "$listdir/$ml.closed");
if ( -e "$listdir/$ml.auto" && -e "$listdir/$ml.closed") {
print STDERR "sowohl $ml.auto als auch $ml.closed existieren. W�hle $ml.closed\n";
}
else {
$mlconf{"subscribe_policy"} = "auto" if ( -e"$listdir/$ml.auto");
$mlconf{"unsubscribe_policy"} = "auto" if ( -e"$listdir/$ml.auto");
}
$mlconf{"strip"} = 1 if ( -e "$listdir/$ml.strip");
$mlconf{"noadvertise"} = "/.*/" if ( -e "$listdir/$ml.hidden");
# admin_passwd:
$filename = "$listdir/" . $mlconf{"admin_passwd"};
if ( -e "$listdir/$ml.passwd" ) {
$mlconf{"admin_passwd"} = read_from_file("$listdir/$ml.passwd");
}
elsif ( -e "$filename" ) {
$mlconf{"admin_passwd"} = read_from_file("$filename");
}
# else take it verbatim
# approve_passwd:
$filename = "$listdir/" . $mlconf{"approve_passwd"};
if ( -e "$listdir/$ml.passwd" ) {
$mlconf{"approve_passwd"} = read_from_file("$listdir/$ml.passwd");
}
elsif ( -e "$filename" ) {
$mlconf{"approve_passwd"} = read_from_file("$filename");
}
# else take it verbatim
#
# c)
# add some information from additional configuration files
#
# restrict_post
if (defined ($mlconf{"restrict_post"})) {
@privileged = ();
for $filename (split /\s+/, $mlconf{"restrict_post"}) {
open (FH, "< $listdir/$filename") or die "can't open $listdir/$filename\n";
push (@privileged, <FH>);
chomp @privileged;
close (FH);
}
}
if ($susehack =~ m/yes/i) {
@privileged = grep(!/$susearchuser\@$mydomain/i, @privileged);
}
$mlconf{"privileged"} = \@privileged;
# members
@members = ();
open (FH, "< $listdir/$ml") or die "can't open $listdir/$ml\n";
push (@members, <FH>);
chomp @members;
close (FH);
$mlconf{"gated"} = "no";
if ($susehack =~ m/yes/i) {
if (grep(/$susearchuser\@$mydomain/i, @members)) {
$mlconf{"gated"} = "yes";
}
@members = grep(!/$susearchuser\@$mydomain/i, @members);
}
$mlconf{"members"} = \@members;
# intro message
if (open (FH, "< $listdir/$ml.intro")) {
{ local $/; $mlconf{"intro"} = <FH>; }
}
else { $mlconf{"intro"} = ""; }
# info message
if (open (FH, "< $listdir/$ml.info")) {
{ local $/; $mlconf{"info"} = <FH>; }
}
else { $mlconf{"info"} = ""; }
#
# d)
# take over some other params into the configuration table
#
$mlconf{"name"} = "$ml";
($primaryowner, @secondaryowner) =
expand_alias (split (/\s*,\s*/, aliassub($mlowners{$ml})));
($primaryapprover, @secondaryapprover) =
expand_alias (split (/\s*,\s*/, aliassub($mlapprovers{$ml})));
$mlconf{"primaryowner"} = $primaryowner;
$mlconf{"secondaryowner"} = \@secondaryowner;
$mlconf{"primaryapprover"} = $primaryapprover;
$mlconf{"secondaryapprover"} = \@secondaryapprover;
#
# debugging output
#
if (defined ($debug)) {
print "##################### $ml ####################\n";
for $skey (sort keys %mlconf) {
if (defined ($mlconf{$skey})) { print "$skey = $mlconf{$skey}\n"; }
else { print "$skey = (?)\n"; }
}
my $priv;
for $priv (@privileged) {
print "\t$ml: $priv\n";
}
}
#
# e)
# with the help of Mailman commands - create a new list and subscribe the old staff
#
if (defined($update)) {
print "updating configuration of \"$ml\"\n";
}
else {
# Mailman lists can initially be only created with one owner
@args = ("$mailmanbin/newlist", "-q", "-o", "$aliasout", "$ml", $mlconf{"primaryowner"}, $mlconf{"admin_passwd"});
system (@args) == 0 or die "system @args failed: $?";
}
# Mailman accepts only subscriber lists > 0
if (@members > 0) {
$filename = tmpnam();
open (FH, "> $filename") or die "can't open $filename\n";
for $skey (@members) {
print FH "$skey" . "\n";
}
close (FH);
@args = ("$mailmanbin/add_members", "-n", "$filename", "--welcome-msg=n", "$ml");
system (@args) == 0 or die "system @args failed: $?";
}
#
# f)
# "translate" the Majordomo list configuration
#
m2m();
# write the Mailman config
$filename = tmpnam();
open (FH, "> $filename") or die "can't open $filename\n";
for $skey (sort keys %mmconf) {
print FH "$skey = " . $mmconf{$skey} . "\n";
}
close (FH);
@args = ("$mailmanbin/config_list", "-i", "$filename", "$ml");
system (@args) == 0 or die "system @args failed: $?";
unlink($filename) or print STDERR "unable to unlink \"$filename\"!\n";
}
exit 0;
#############
# subs
#############
#
# I don't know how to write Perl code
# therefor I need this stupid procedure to cleanly read a value from file
#
sub read_from_file {
my $value;
local *FH;
open (FH, "< $_[0]") or die "can't open $_[0]\n";
$value = <FH>;
chomp $value;
close (FH);
return $value;
}
#
# add "@$mydomain" to each element that does not contain a "@"
#
sub expand_alias {
return map { (not $_ =~ /@/) ? $_ .= "\@$mydomain" : $_ } @_;
}
#
# replace the typical owner-majordomo aliases
#
sub aliassub {
my $string = $_[0];
$string =~ s/(owner-$majordomo|$majordomo-owner)/mailman-owner/gi;
return $string;
}
#
# default values of Majordomo mailing lists
# (stolen from majordomo::config_parse.pl: %known_keys)
#
sub init_defaultmlconf {
my $ml = $_[0];
%defaultmlconf=(
'welcome', "yes",
'announcements', "yes",
'get_access', "open",
'index_access', "open",
'who_access', "open",
'which_access', "open",
'info_access', "open",
'intro_access', "open",
'advertise', "",
'noadvertise', "",
'description', "",
'subscribe_policy', "open",
'unsubscribe_policy', "open",
'mungedomain', "no",
'admin_passwd', "$ml.admin",
'strip', "yes",
'date_info', "yes",
'date_intro', "yes",
'archive_dir', "",
'moderate', "no",
'moderator', "",
'approve_passwd', "$ml.pass",
'sender', "owner-$ml",
'maxlength', "40000",
'precedence', "bulk",
'reply_to', "",
'restrict_post', "",
'purge_received', "no",
'administrivia', "yes",
'resend_host', "",
'debug', "no",
'message_fronter', "",
'message_footer', "",
'message_headers', "",
'subject_prefix', "",
'taboo_headers', "",
'taboo_body', "",
'digest_volume', "1",
'digest_issue', "1",
'digest_work_dir', "",
'digest_name', "$ml",
'digest_archive', "",
'digest_rm_footer', "",
'digest_rm_fronter', "",
'digest_maxlines', "",
'digest_maxdays', "",
'comments', ""
);
}
#
# Mailman mailing list params that are not derived from Majordomo mailing lists params
# (e.g. bounce_matching_headers+forbbiden_posters vs. taboo_headers+taboo_body)
# If you need one of this params to be variable remove it here and add some code to the
# main procedure; additionally, you should compare it with what you have in
# /usr/lib/mailman/Mailman/mm_cfg.py
#
sub init_defaultmmconf {
%defaultmmconf=(
'goodbye_msg', "\'\'",
'umbrella_list', "0",
'umbrella_member_suffix', "\'$umbrella_member_suffix\'",
'send_reminders', "0",
'admin_immed_notify', "1",
'admin_notify_mchanges', "0",
'dont_respond_to_post_requests', "0",
'obscure_addresses', "1",
'require_explicit_destination', "1",
'acceptable_aliases', "\"\"\"\n\"\"\"\n",
'max_num_recipients', "10",
'forbidden_posters', "[]",
'bounce_matching_headers', "\"\"\"\n\"\"\"\n",
'anonymous_list', "0",
'nondigestable', "1",
'digestable', "1",
'digest_is_default', "0",
'mime_is_default_digest', "0",
'digest_size_threshhold', "40",
'digest_send_periodic', "1",
'digest_header', "\'\'",
'bounce_processing', "1",
'minimum_removal_date', "4",
'minimum_post_count_before_bounce_action', "3",
'max_posts_between_bounces', "5",
'automatic_bounce_action', "3",
'archive_private', "0",
'clobber_date', "1",
'archive_volume_frequency', "1",
'autorespond_postings', "0",
'autoresponse_postings_text', "\'\'",
'autorespond_admin', "0",
'autoresponse_admin_text', "\'\'",
'autorespond_requests', "0",
'autoresponse_request_text', "\'\'",
'autoresponse_graceperiod', "90"
);
}
#
# convert a Majordomo mailing list configuration (%mlconf) into a
# Mailman mailing list configuration (%mmconf)
# only those params are affected which can be derived from Majordomo
# mailing list configurations
#
sub m2m {
my $elem;
my $admin;
$mmconf{"real_name"} = "\'" . $mlconf{"name"} . "\'";
# Mailman does not know the difference between owner and approver
for $admin (($mlconf{"primaryowner"}, @{$mlconf{"secondaryowner"}},
$mlconf{"primaryapprover"}, @{$mlconf{"secondarapprover"}})) {
# merging owners and approvers may result in a loop:
if (lc($admin) ne lc("owner-" . $mlconf{"name"} . "\@" . $mydomain)) {
$mmconf{"owner"} .= ",\'" . "$admin" . "\'";
}
}
$mmconf{"owner"} =~ s/^,//g;
$mmconf{"owner"} = "\[" . $mmconf{"owner"} . "\]";
# remove characters that will break Python
($mmconf{"description"} = $mlconf{"description"}) =~ s/\'/\\\'/g;
$mmconf{"description"} = "\'" . $mmconf{"description"} . "\'";
$mmconf{"info"} = "\"\"\"\n" . $mlconf{"info"} . "\"\"\"\n";
$mmconf{"subject_prefix"} = "\'" . $mlconf{"subject_prefix"} . "\'";
$mmconf{"welcome_msg"} = "\"\"\"\n" . $mlconf{"intro"} . "\"\"\"\n";
# I don't know how to handle this because the reply_to param in the lists
# I had were not configured consistently
if ($mlconf{"reply_to"} =~ /\S+/) {
if ($mlconf{"name"} . "\@" =~ m/$mlconf{"reply_to"}/i) {
$mmconf{"reply_goes_to_list"} = "1";
$mmconf{"reply_to_address"} = "\'\'";
}
else {
$mmconf{"reply_goes_to_list"} = "2";
$mmconf{"reply_to_address"} = "\'" . $mlconf{"reply_to"} . "\'";
}
}
else {
$mmconf{"reply_goes_to_list"} = "0";
$mmconf{"reply_to_address"} = "\'\'";
}
$mmconf{"administrivia"} = ($mlconf{"administrivia"} =~ m/yes/i) ? "1" : "0";
$mmconf{"send_welcome_msg"} = ($mlconf{"welcome"} =~ m/yes/i) ? "1" : "0";
$mmconf{"max_message_size"} = int ($mlconf{"maxlength"} / 1000);
$mmconf{"host_name"} = ($mlconf{"resend_host"} =~ /\S+/) ?
$mlconf{"resend_host"} : "\'" . $mydomain . "\'";
$mmconf{"web_page_url"} = "\'" . $myurl . "\'";
# problematic since Mailman does not know access patterns
# I assume, that if there was given a noadvertise pattern, the
# list shouldn't be visible at all
$mmconf{"advertised"} = ($mlconf{"noadvertise"} =~ /\.\*/) ? "0" : "1";
# confirm+approval is much to long winded for private sites
$mmconf{"subscribe_policy"} =
($mlconf{"subscribe_policy"} =~ m/(open|auto)/i) ? "1" :
($private =~ m/yes/i) ? "2" : "3";
# in case this is a private site allow list visiblity at most
$mmconf{"private_roster"} =
($mlconf{"who_access"} =~ m/open/i and not $private =~ m/yes/i) ? "0" :
($mlconf{"who_access"} =~ m/open|list/i) ? "1" : "2";
$mmconf{"moderated"} = ($mlconf{"moderate"} =~ m/yes/i) ? "1" : "0";
# there is no way to a set a separate moderator in Mailman
# external, since lengthy
mm_posters();
if ($mlconf{"message_fronter"} =~ /\S+/) {
$mmconf{"msg_header"} = "\"\"\"\n" . $mlconf{"message_fronter"} . "\"\"\"\n";
}
else {
$mmconf{"msg_header"} = "\'\'";
}
if ($mlconf{"message_footer"} =~ /\S+/) {
$mmconf{"msg_footer"} = "\"\"\"\n" . $mlconf{"message_footer"} . "\"\"\"\n";
}
else {
$mmconf{"msg_footer"} = "\'\'";
}
# gateway to news
$mmconf{"nntp_host"} = "\'" . $newsserver . "\'";
$mmconf{"linked_newsgroup"} = "\'" . $newsprefix . $mlconf{"name"} . "\'";
if ($mlconf{"gated"} =~ m/yes/i) {
$mmconf{"gateway_to_news"} = "1";
$mmconf{"gateway_to_mail"} = "1";
$mmconf{"archive"} = "1";
}
else {
$mmconf{"gateway_to_news"} = "0";
$mmconf{"gateway_to_mail"} = "0";
$mmconf{"archive"} = "0";
}
# print warnings if this seems to be an umbrella list
for $elem (@{$mlconf{"privileged"}}, @{$mlconf{"members"}}) {
$elem =~ s/\@$mydomain//gi;
if (defined($mlaliases{$elem . $umbrella_member_suffix})) {
print STDERR "\"" . $mlconf{"name"} .
"\" possibly forms part off/is an umbrella list, since \"$elem\" is a local mailing list alias\n";
}
}
# print warnings if we encountered a Taboo-Header or Taboo-Body
if ($mlconf{"taboo_headers"} =~ /\S+/ or $mlconf{"taboo_body"} =~ /\S+/) {
print STDERR "\"" . $mlconf{"name"} . "\" taboo_headers or taboo_body seem to be set - please check manually.\n";
}
}
#
# with some set theory on the member and priviliged list try to determine the params
# $mmconf{"member_posting_only"} and $mmconf{"posters"}
#
sub mm_posters {
if ($mlconf{"restrict_post"} =~ /\S+/) {
my %privileged = ();
my %members = ();
my $key;
foreach $key (@{$mlconf{"privileged"}}) { $privileged{$key} = "OK"; }
foreach $key (@{$mlconf{"members"}}) { $members{$key} = "OK"; }
# are all members privileged, too ?
my $included = 1;
foreach $key (keys %members) {
if (not exists $privileged{$key}) {
$included = 0;
last;
}
}
if ($included) {
$mmconf{"member_posting_only"} = "1";
# posters = privileged - members:
my %diff = %privileged;
foreach $key (keys %members) {
delete $diff{$key} if exists $members{$key};
}
$mmconf{"posters"} = "";
for $key (sort keys %diff) {
$mmconf{"posters"} .= ",\'" . $key . "\'";
}
$mmconf{"posters"} =~ s/^,//g;
$mmconf{"posters"} = "[" . $mmconf{"posters"} . "]";
}
else {
$mmconf{"member_posting_only"} = "0";
# posters = privileged:
$mmconf{"posters"} = "";
for $key (sort keys %privileged) {
$mmconf{"posters"} .= ",\'" . $key . "\'";
}
$mmconf{"posters"} =~ s/^,//g;
$mmconf{"posters"} = "[" . $mmconf{"posters"} . "]";
}
}
else {
$mmconf{"member_posting_only"} = "0";
$mmconf{"posters"} = "[]";
}
}