aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/majordomo2mailman.pl
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/majordomo2mailman.pl')
-rw-r--r--contrib/majordomo2mailman.pl691
1 files changed, 691 insertions, 0 deletions
diff --git a/contrib/majordomo2mailman.pl b/contrib/majordomo2mailman.pl
new file mode 100644
index 00000000..c874862e
--- /dev/null
+++ b/contrib/majordomo2mailman.pl
@@ -0,0 +1,691 @@
+#!/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"} = "[]";
+ }
+}
+