diff options
| author | Guilhem Moulin <guilhem.moulin@fripost.org> | 2013-01-29 02:39:17 +0100 | 
|---|---|---|
| committer | Guilhem Moulin <guilhem.moulin@fripost.org> | 2013-01-29 02:39:17 +0100 | 
| commit | 38bbf969d6c29891f40973a0db376d5f5ee5ab07 (patch) | |
| tree | 2b0e4a02308b06d14a4361786acf8deb209d9d3d | |
| parent | 7b81775603b8208c995cd1c4a15cd2a287009404 (diff) | |
Factorized the code to add localparts.
| -rw-r--r-- | lib/Fripost/Panel/Interface.pm | 309 | ||||
| -rw-r--r-- | lib/Fripost/Schema/Local.pm | 371 | ||||
| -rw-r--r-- | templates/add-alias.html | 18 | ||||
| -rw-r--r-- | templates/add-list.html | 23 | ||||
| -rw-r--r-- | templates/add-user.html | 18 | ||||
| -rw-r--r-- | templates/edit-alias.html | 20 | ||||
| -rw-r--r-- | templates/edit-list.html | 16 | ||||
| -rw-r--r-- | templates/edit-user.html | 26 | ||||
| -rw-r--r-- | templates/list-locals.html | 4 | 
9 files changed, 426 insertions, 379 deletions
diff --git a/lib/Fripost/Panel/Interface.pm b/lib/Fripost/Panel/Interface.pm index a0c9dd9..7f7d770 100644 --- a/lib/Fripost/Panel/Interface.pm +++ b/lib/Fripost/Panel/Interface.pm @@ -216,8 +216,8 @@ sub ListLocals : Runmode {      my @lists   = grep { $_->{type} eq 'list' } @locals;      # Add a link to the list (external) homepage. -    map { $_->{listURL} = $CFG{'listurl_'.$_->{transport}}. -                          email_to_ascii($_->{name}.'@'.$domainname) } +    map { $_->{list_URL} = $CFG{'listurl_'.$_->{transport}}. +                           email_to_ascii($_->{name}.'@'.$domainname) }          @lists;      my $template = $self->load_tmpl( 'list-locals.html', cache => 1, @@ -256,258 +256,183 @@ sub ListLocals : Runmode {      # Can the user add lists?      $template->param( canAddList => $domain->{permissions} =~ /[lop]/ );      $template->param( listCanAddList => [ map { {item => encode_entities($_)} } -                                                @{$domain->{canAddList}} ] ) +                                              @{$domain->{canAddList}} ] )          if $domain->{permissions} =~ /[op]/;      # Should we list lists?      $template->param( listLists => $#lists >= 0 ||                                     $domain->{permissions} =~ /[lop]/ );      $template->param( lists => [ -        map { {&fill_HTML_template_from_entry ($_, -loop => ['destination'])} } +        map { { &fill_HTML_template_from_entry ($_, -loop => ['destination'] ) +              , isPending => $_->{isPending} +              } }              @lists ]);      return $template->output;  } -# In this Run Mode authenticated users can edit the entry (if they have -# the permission). -sub EditLocal : Runmode { +# In this Run Mode authenticated users can add users, aliases and lists +# (if they have the permission). +sub AddLocal : Runmode {      my $self = shift;      my %CFG = $self->cfg;      my $q = $self->query; -    return $self->redirect('../') if defined $q->param('cancel'); +    return $self->redirect('./') if defined $q->param('cancel'); # Cancellation +    # Get the domain name from the URL. +    my $domainname = ($self->split_path)[1]; +    my $t = $q->param('t') // return $self->redirect('./'); +    return $self->redirect('./') unless grep { $t eq $_ } qw/user alias list/;      my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); -    # Search for *the* matching user, alias or list. -    my ($d,$l) = ($self->split_path)[1,2]; -    $fp->domain->get ($d, -die => 404, -assert_exist => 1); -    my %local = $fp->local->get ($l.'@'.$d, -die => 404, -                                            -concat => "\x{0D}\x{0A}" ); -    die "Unknown type" unless grep { $local{type} eq $_ } -                                   qw/user alias list/; -    die "404\n" if $local{ispending}; -      my $error; # Tells whether the change submission has failed. -    my $t = $local{type}; - -    if (defined $q->param('a') and $q->param('a') eq 'delete') { -        # Delete the entry -        $error = $fp->$t->delete($l.'@'.$d, -die => 0); -        unless ($error) { -            $fp->done; -            return $self->redirect('../'); -        } -    }      if (defined $q->param('submit')) {          # Changes have been submitted: process them -        my %entry; -        if ($t eq 'user') { -            $entry{user} = $l.'@'.$d; -            $entry{forwards} = $q->param('forwards') // undef; - -            if (($q->param('oldpw')  // '') ne '' or -                ($q->param('newpw')  // '') ne '' or -                ($q->param('newpw2') // '') ne '') { -                # If the user tries to change the password, we make her -                # bind first, to prevent an attacker from setting a -                # custom password and accessing the emails. -                if ($q->param('newpw') ne $q->param('newpw2')) { -                    $error = "Passwords do not match"; -                } -                elsif (length $q->param('newpw') < $CFG{password_min_length}) { -                    $error = "Password should be at least " -                            .$CFG{password_min_length} -                            ." characters long."; -                } -                else { -                    my $fp; -                    eval { -                        my $u = email_to_unicode($self->authen->username); -                        $fp = Fripost::Schema::->auth( -                                  $u, -                                  $q->param('oldpw') // '', -                                  %CFG, -                                  -die => "Wrong password (for ‘".$u."’)." ); -                    }; -                    $error = $@ || $fp->user->passwd( -                                      $entry{user}, -                                      Fripost::Password::hash($q->param('newpw') // '') -                                   ); -                    $fp->done if defined $fp; -                } +        my $local = &parse_CGI_query($q); +        $local->{type} = $q->param('t'); +        $local->{name} = $q->param('name').'@'.$domainname; +        my %rest; + +        if ($q->param('password') || $q->param('password2')) { +            if ($q->param('password') ne $q->param('password2')) { +                $error = "Passwords do not match";              } +            # TODO: ! move that to Password.pm +            elsif (length $q->param('password') < $CFG{password_min_length}) { +                $error = "Password should be at least " +                        .$CFG{password_min_length} +                        ." characters long."; +            } +            else { +                $local->{password} = Fripost::Password::hash($q->param('password')); +            } +            # TODO: inherit the user quota from the postmaster's?          } -        elsif ($t eq 'alias') { -            $entry{alias} = $l.'@'.$d; -            $entry{maildrop} = $q->param('maildrop') // undef; -        } -        elsif ($t eq 'list') { -            $entry{list} = $l.'@'.$d; -            $entry{transport} = $q->param('transport') // undef; -        } -        $entry{isactive} = $q->param('isactive') // 1; -        $entry{description} = $q->param('description') // undef; -        $error = $fp->$t->replace( \%entry, -concat => "(\n|\x{0D}\x{0A})") -            unless $error; -    } +        $local->{password} = $q->param('password') if $t eq 'list'; -    my $template = $self->load_tmpl( "edit-$t.html", cache => 1 ); -    $template->param( $self->userInfo ); -    $template->param( domain => encode_entities($d) ); +        $rest{gpg} = { use_agent => 0 +                     , keydir => $CFG{gpghome} +                     , key => $CFG{gpg_private_key_id} +                     , passphrase => $CFG{gpg_private_key_passphrase} +                     }; -    if ($error and defined $q->param('submit')) { -        # Preserve the (incorrect) form, except the passwords -        if ($t eq 'user') { -            $template->param( user => encode_entities($l) -                            , forwards => $q->param('forwards') // undef ); -        } -        elsif ($t eq 'alias') { -            $template->param( alias => encode_entities($l) -                            , maildrop => $q->param('maildrop') // undef ); -        } -        elsif ($t eq 'list') { -            $template->param( list => encode_entities($l) ); -        } -        $template->param( isactive => $q->param('isactive') // 1 -                        , description => $q->param('description') // undef ); -    } -    else { -        %local = $fp->local->get ($l.'@'.$d, -die => 404, -                                             -concat => "\x{0D}\x{0A}" ); -        if ($t eq 'user') { -            $template->param( user => encode_entities($local{user}) -                            , forwards => encode_entities($local{forwards}) ); -        } -        elsif ($t eq 'alias') { -            $template->param( alias => encode_entities($local{alias}) -                            , maildrop => encode_entities($local{maildrop}) ); -        } -        elsif ($t eq 'list') { -            $template->param( list => encode_entities($local{list}) ); +        unless ($error) { +            my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); +            $fp->domain->search ($domainname, -filter => 'unlocked', -count => 1) +                or die "404\n"; +            $fp->local->add( $local, %rest, -error => \$error ); +            $fp->done; +            return $self->redirect('./') unless $error;          } -        $template->param( isactive => $local{isactive} -                        , description => $local{description} );      } -    $fp->done; -    my $news = (defined $q->param('submit') or -                (defined $q->param('a') and $q->param('a') eq 'delete')); -    $template->param( newChanges => $news ); + +    # Do not send passwords back to the sender. +    $q->delete(qw/password password2/); + +    my $template = $self->load_tmpl( "add-$t.html", cache => 1 ); +    $template->param( $self->userInfo +                    , domainname => encode_entities($domainname) +                    , &fill_HTML_template_from_query ($q)); +    $template->param( transport => +            [ { item => 'mailman', selected => $q->param('transport') eq 'mailman', name => 'GNU Mailman' } +            , { item => 'schleuder', selected => $q->param('transport') eq 'schleuder', name => 'Schleuder' } +            ]) # TODO ugly +        if $t eq 'list' and defined $q->param('transport');      $template->param( error => encode_entities ($error) ) if $error; -    $template->param( canDelete => 1 ) if $t eq 'alias'; -    $template->param( listURL => $CFG{'listurl_'.$local{transport}}. -                                 email_to_ascii($l.'@'.$d) ) -        if $t eq 'list'; -    $q->delete('a');      return $template->output;  } - -# In this Run Mode authenticated users can add users, aliases and lists -# (if they have the permission). -sub AddLocal : Runmode { +# In this Run Mode authenticated users can edit the entry (if they have +# the permission). +sub EditLocal : Runmode {      my $self = shift;      my %CFG = $self->cfg;      my $q = $self->query; -    return $self->redirect('./') if defined $q->param('cancel'); +    return $self->redirect('./') if defined $q->param('cancel'); # Cancellation + +    # Get the domain name from the URL. +    my ($localname,$domainname) = ($self->split_path)[2,1]; +    my $name = $localname.'@'.$domainname; +    my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); + +    # Search for *the* matching user, alias or list. +    $fp->domain->search ($domainname, -filter => 'unlocked', -count => 1) +        or die "404\n"; +    my $local = $fp->local->search ($name, -filter => 'unlocked') +        or die "404\n"; -    my $d = ($self->split_path)[1]; -    my $t = $q->param('t') // die "Undefined type";      my $error; # Tells whether the change submission has failed. +    if (defined $q->param('a') and $q->param('a') eq 'delete') { +        # Delete the entry +        $fp->local->delete($name, -error => \$error ); +        unless ($error) { +            $fp->done; +            return $self->redirect('../'); +        } +    } +    $fp->done; +      if (defined $q->param('submit')) {          # Changes have been submitted: process them -        my %entry; +        my $local = &parse_CGI_query($q); +        $local->{type} = $q->param('t'); +        $local->{name} = $name;          my %rest; -        if ($t eq 'user') { -            $entry{user} = $q->param('user').'@'.$d; -            $entry{forwards} = $q->param('forwards'); -            if ($q->param('password') ne $q->param('password2')) { -                $error = "Passwords do not match"; -            } -            elsif (length $q->param('password') < $CFG{password_min_length}) { -                $error = "Password should be at least " -                        .$CFG{password_min_length} -                        ." characters long."; -            } -            else { -                $entry{password} = Fripost::Password::hash($q->param('password')); -            } -            # TODO: inherit the quota from the postmaster's? -        } -        elsif ($t eq 'alias') { -            $entry{alias} = $q->param('alias').'@'.$d; -            $entry{maildrop} = $q->param('maildrop'); -        } -        elsif ($t eq 'list') { -            $entry{list} = $q->param('list').'@'.$d; -            $entry{transport} = $q->param('transport'); + +        if ($q->param('password') || $q->param('password2')) {              if ($q->param('password') ne $q->param('password2')) {                  $error = "Passwords do not match";              } +            # TODO: ! move that to Password.pm +            # TODO: change password              elsif (length $q->param('password') < $CFG{password_min_length}) {                  $error = "Password should be at least "                          .$CFG{password_min_length}                          ." characters long.";              }              else { -                $rest{gpg} = { use_agent => 0 -                             , keydir => $CFG{gpghome} -                             , key => $CFG{gpg_private_key_id} -                             , passphrase => $CFG{gpg_private_key_passphrase} -                             }; -                $entry{password} = $q->param('password'); +                $local->{password} = Fripost::Password::hash($q->param('password'));              }          } -        else { -            # Unknown type -            return $self->redirect('./'); -        } -        $entry{isactive} = $q->param('isactive') // 1; -        $entry{description} = $q->param('description') // undef; - -        unless ($error) { -            my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); -            $fp->domain->get ($d, -die => 404, -assert_exist => 1); -            $error = $fp->$t->add( \%entry, -concat => "(\n|\x{0D}\x{0A})", %rest); -            $fp->done; -            return $self->redirect('./') unless $error; -        }      } -    my $template = $self->load_tmpl( "add-$t.html", cache => 1 ); -    $template->param( $self->userInfo ); -    $template->param( domain => encode_entities($d) ); +    # Do not send passwords back to the sender. +    $q->delete(qw/password password2/); + +    my $t = $local->{type}; +    my $template = $self->load_tmpl( "edit-$t.html", cache => 1 ); +    $template->param( $self->userInfo +                    , localpart => encode_entities($localname) +                    , domainpart => encode_entities($domainname) ); +      if ($error) {          # Preserve the (incorrect) form, except the passwords -        if ($t eq 'user') { -            $template->param( user => $q->param('user') // undef -                            , forwards => $q->param('forwards') // undef ); -        } -        elsif ($t eq 'alias') { -            $template->param( alias => $q->param('alias') // undef -                            , maildrop => $q->param('maildrop') // undef ); -        } -        elsif ($t eq 'list') { -            $template->param( list => $q->param('list') // undef -                            , isenc => $q->param('transport') eq 'schleuder' ); -        } -        else { -            # Unknown type -            return $self->redirect('./'); -        } -        $template->param( isactive => $q->param('isactive') // 1 -                        , description => $q->param('description') // undef -                        , error => encode_entities ($error) ); +        $template->param( &fill_HTML_template_from_query ($q) );      }      else { -        $template->param( isactive => 1 ); +        $template->param( &fill_HTML_template_from_entry ($local, +                -hide => [qw/quota transport/]) );      } +    # TODO: submit +    my $news = (defined $q->param('submit') or +               (defined $q->param('a') and $q->param('a') eq 'delete')); +    $template->param( newChanges => $news ); +    $template->param( error => encode_entities ($error) ) if $error; +    $template->param( canDelete => 1 ) if $t eq 'alias'; +    $template->param( list_URL => $CFG{'listurl_'.$local->{transport}}. +                                  email_to_ascii($name) ) +        if $t eq 'list'; + +    $q->delete('a');      return $template->output;  } + + +  sub mkURL {      my $host = shift;      my @path = map { encodeURIComponent($_) } @_; diff --git a/lib/Fripost/Schema/Local.pm b/lib/Fripost/Schema/Local.pm index 1f09f66..d6e32a2 100644 --- a/lib/Fripost/Schema/Local.pm +++ b/lib/Fripost/Schema/Local.pm @@ -18,7 +18,8 @@ use utf8;  use parent 'Fripost::Schema';  use Fripost::Schema::Util qw/concat split_addr canonical_dn -                             ldap_error dn2mail/; +                             ldap_error dn2mail softdie email_valid +                             ldap_assert_absent/;  use Net::IDN::Encode qw/email_to_ascii email_to_unicode/;  use Net::LDAP::Util 'escape_filter_value'; @@ -58,6 +59,15 @@ them.  (User only) A string e.g., C<100 MB> representing the current quota on  the user's mailboxes. +=item B<password> + +(User and list only) The user or list administrator password. It is +never given back by the server (actually noone has read access on that +attribute), hence only makes sense upon creation. In users entries, +I<password> can be hashed on the client side when prefixed with +B<{SHA}>, B<{SSHA}>, B<{MD5}>, B<{SMD5}>, B<{CRYPT}> or B<{CLEARTEXT}>. +(Otherwise the password will be automatically salted and SHA-1 hashed.) +  =item B<owner>  (Alias and list only) An optional array reference containing the @@ -140,15 +150,12 @@ B<Fripost::Schema::Util> for details.  sub search {      my $self = shift; -    my $in = shift; +    my ($localname, $domainname) = split_addr(shift);      my %options = @_;      # Nothing to do after an error.      return if $options{'-error'} && ${$options{'-error'}}; -    # If there is not '@', we interpret $in as a domain name. -    $in =~ s/^([^\@]+)$/\@$1/; -    my ($localname, $domainname) = split_addr($in);      my @filters;      if (defined $options{'-type'}) { @@ -218,8 +225,8 @@ sub search {      my $count = 0;      my @resultset;      foreach my $domainname (@domainnames) { -        # For query the server for each matching domain. -        my $locals = $self->ldap->search( base => $self->mail2dn('@'.$domainname) +        # We query the server for each matching domain. +        my $locals = $self->ldap->search( base => $self->mail2dn($domainname)                                          , scope => 'one'                                          , deref => 'never'                                          , filter => $filter @@ -313,7 +320,7 @@ sub _entries_to_locals {                      if not @$keys or grep { $_ eq 'transport' } @$keys;              }              else { -                die "Missing translation for local attribute ‘".$attr."’."; +                die "Missing translation for local attribute ‘".$attr."’";              }          } @@ -324,153 +331,267 @@ sub _entries_to_locals {      return @locals;  } +# Map our domain keys into the LDAP attribute(s) that are required to +# fetch this information. +sub _keys_to_attrs { +    my %map = ( name => 'fvl' +              , type => 'objectClass' +              , isActive => 'fripostIsStatusActive' +              , description => 'description' +              , isPending => 'objectClass' +              , quota => 'fripostUserQuota' +              , owner => 'fripostOwner' +              , forward => 'fripostOptionalMaildrop' +              , destination => 'fripostMaildrop' +              , transport => 'fripostListManager' +              ); +    my %attrs; +    foreach my $k (@_) { +        die "Missing translation for key ‘".$k."’" +            unless exists $map{$k}; +        if (ref $map{$k} eq 'ARRAY') { +            $attrs{$_} = 1 for @{$map{$k}}; +        } +        else { +            $attrs{$map{$k}} = 1; +        } +    } +    return keys %attrs; +} -=head1 METHODS - -=over 4 - -=item B<get> (I<local>, I<OPTIONS>) - -Returns a hash with all the (visible) attributes for the given entry. An -additional 'type' attribute gives the type of *the* found entry -(possible values are 'user', 'alias', and 'list'). - -=cut +my %list_commands = ( mailman => [ qw/admin bounces confirm join leave +                                      owner request subscribe unsubscribe/ ] +                    , schleuder => [ qw/bounce sendkey/ ] +                    ); -sub get { +sub add {      my $self = shift; -    my $loc = shift; +    my $local = shift;      my %options = @_; -    my $concat = $options{'-concat'}; - -    my ($l,$d) = split_addr( $loc, -encode => 'ascii' ); -    $l = escape_filter_value($l); -    my $locals = $self->ldap->search( -                     base => canonical_dn({fvd => $d}, @{$self->suffix}), -                     scope => 'one', -                     deref => 'never', -                     filter => "(|(&(objectClass=FripostVirtualUser)(fvu=$l)) -                                  (&(objectClass=FripostVirtualAlias)(fva=$l)) -                                  (&(objectClass=FripostVirtualList)(fvl=$l)))", -                     attrs => [ qw/fvu description -                                   fripostIsStatusActive -                                   fripostOptionalMaildrop -                                   fripostUserQuota -                                   fva fripostMaildrop -                                   fvl fripostListManager/ ] -    ); -    if ($locals->code) { -        die $options{'-die'}."\n" if defined $options{'-die'}; -        die $locals->error."\n"; -    } -    # The following is not supposed to happen. Note that there is -    # nothing in the LDAP schema to prevent that, but it's not too -    # critical as Postfix searchs for user, aliases and lists in -    # that order. -    die "Error: Multiple matching entries found." if $locals->count > 1; -    my $local = $locals->pop_entry; - -    unless (defined $local) { -        die $options{'-die'}."\n" if defined $options{'-die'}; -        die "No such such entry ‘".$loc."’.\n"; +    # Nothing to do after an error. +    return if $options{'-error'} && ${$options{'-error'}}; +    softdie ("No name specified", %options) // return +        unless $local->{name} =~ /^.+\@[^\@]+$/; + +    my $name = $local->{name}; +    my ($localname, $domainname) = split_addr($name); +    # Check validity. +    &_assert_valid($local, %options) // return; + +    my $exists; +    my $t = $local->{type}; +    if ($options{'-dry-run'} or $options{'-append'}) { +        # Search for an existing entry with the same name. We can't +        # use our previously defined method here, since the current user +        # may not have read access to the entry. There is a race +        # condition since someone could modify the directory between +        # this check and the actual insertion, but then the insertion +        # would fail. +        $exists = ldap_assert_absent( $self, $name, undef, %options ) // return; + +        if ($t eq 'list') { +            # Ensure that all commands are available. +            foreach (@{$list_commands{$local->{transport}}}) { +                 my $name = $localname.'-'.$_.'@'.$domainname; +                 ldap_assert_absent( $self, $name, undef, %options ) // return; +            } +        } +        return 1 if $options{'-dry-run'};      } -    my %ret; -    if ($local->dn =~ /^fvu=/) { -        $ret{type} = 'user'; -        $ret{user} = $local->get_value('fvu'); -        $ret{forwards} = concat($concat, map { email_to_unicode($_) } -                                         $local->get_value('fripostOptionalMaildrop')) -    } -    elsif ($local->dn =~ /^fva=/) { -        $ret{type} = 'alias'; -        $ret{alias} = $local->get_value('fva'); -        $ret{maildrop} = concat($concat, map { email_to_unicode($_) } -                                         $local->get_value('fripostMaildrop')) +    # Convert the domain into a LDAP entry, and remove keys to empty values. +    my %attrs = $self->_local_to_entry (%$local); +    Fripost::Schema::Util::ldap_clean_entry( \%attrs ); + +    my $mesg; +    my $dn = $self->mail2dn( $local->{name} ); +    if ($options{'-append'} and $exists) { +        # Replace single valued attributes; Add other attributes. +        my %unique; +        foreach (qw/fripostIsStatusActive userPassword fripostUserQuota/) { +            $unique{$_} = delete $attrs{$_} if exists $attrs{$_}; +        } +        $mesg = $self->ldap->modify( $dn, replace => \%unique, add => \%attrs );      } -    elsif ($local->dn =~ /^fvl=/) { -        $ret{type} = 'list'; -        $ret{list} = $local->get_value('fvl'); -        $ret{transport} = $local->get_value('fripostListManager'); +    else { +        # The default owner is the current user. +        $attrs{fripostOwner} //= [ $self->whoami ] unless $t eq 'user'; +        my $die = exists $options{'-die'}; +        $options{'-die'} = { Net::LDAP::Constant::LDAP_ALREADY_EXISTS => +                                "‘".$name."’ exists" +                           , Net::LDAP::Constant::LDAP_SUCCESS => 0 } +            unless $die; + +        if ($t eq 'list') { +            # Lists need special care since we have to create the +            # commands as well, and we need to communicate with the list +            # manager. +            my $pw = delete $attrs{userPassword}; +            $attrs{objectClass} = [ qw/FripostVirtualList FripostPendingEntry/ ]; +            $attrs{fripostLocalAlias} = &_mkLocalAlias($name); + +            my @done; +            my $res = $self->ldap->add( $dn, attrs => [ %attrs ] ); +            push @done, $dn unless $res->code; + +            foreach (@{$list_commands{$local->{transport}}}) { +                # Create the commands; Stop if something goes wrong +                last if $res->code; +                my $name = $localname.'-'.$_.'@'.$domainname; +                $options{'-die'} = { Net::LDAP::Constant::LDAP_ALREADY_EXISTS => +                                        "‘".$name."’ exists" +                                   , Net::LDAP::Constant::LDAP_SUCCESS => 0 } +                    unless $die; +                my %attrs = ( objectClass => [ qw/FripostVirtualListCommand +                                                  FripostPendingEntry/ ] +                            , fripostLocalAlias => &_mkLocalAlias($name) +                            ); +                my $dn = $self->mail2dn( $name ); +                $res = $self->ldap->add( $dn, attrs => [ %attrs ] ); +                push @done, $dn unless $res->code; +            } +            $mesg = $res; +            if ($mesg->code) { +                # Something went wrong. We try to clean up after us, and +                # delete the bogus entries we created. +                # It's not too bad if it doesn't work out, because +                # it'll be cleaned by our service hopefully. +                $self->ldap->delete($_) for @done; +                ldap_error($mesg, %options); +                return; +            } + +            # TODO: send a signed + encrypted mail +        } +        else { +            $attrs{objectClass} =  $t eq 'user' ? 'FripostVirtualUser' : +                                   $t eq 'alias'? 'FripostVirtualAlias' : +                                   ''; +            $mesg = $self->ldap->add( $dn, attrs => [ %attrs ] ); +            # TODO: send a welcome mail? +        }      } -    $ret{isactive} = $local->get_value('fripostIsStatusActive') eq 'TRUE'; -    $ret{description} = concat($concat, $local->get_value('description')); -    $ret{ispending} = ($local->get_value('fripostIsStatusPending') // '') eq 'TRUE'; -    return %ret; +    ldap_error($mesg, %options) // return; +    1;  } -=item B<exists> (I<local>, I<OPTIONS>) +# Convert our representation of local entries into a hash which keys are +# LDAP attributes. +sub _local_to_entry { +    my $self = shift; +    my %local = @_; +    my %entry; + +    foreach my $key (keys %local) { +        if ($key eq 'name') { +            # Its value is forced by the DN. +        } +        elsif ($key eq 'type') { +            # We fix that one later. +        } +        elsif ($key eq 'isActive') { +            $entry{fripostIsStatusActive} = $local{isActive} ? 'TRUE' : 'FALSE'; +        } +        elsif ($key eq 'description') { +            $entry{description} = $local{description}; +        } +        elsif ($key eq 'quota') { +            $entry{fripostUserQuota} = $local{quota}; +        } +        elsif ($key eq 'owner') { +            $entry{fripostOwner} = +                [ map { $self->mail2dn($_) } @{$local{owner}} ]; +        } +        elsif ($key eq 'forward') { +            $entry{fripostOptionalMaildrop} = $local{forward}; +        } +        elsif ($key eq 'destination') { +            $entry{fripostMaildrop} = $local{destination}; +        } +        elsif ($key eq 'transport') { +            $entry{fripostListManager} = $local{transport}; +        } +        elsif ($key eq 'password') { +            $entry{userPassword} = $local{password}; +        } +        else { +            die "Missing translation for key ‘".$key."’"; +        } +    } +    return %entry; +} -Returns 1 if the given I<local>@I<domain> exists, and 0 otherwise. -The authenticated user needs to have search access to the 'entry' -attribute. -=cut -sub exists { -    my $self = shift; -    my ($l,$d) = split_addr( shift, -encode => 'ascii' ); +sub _assert_valid { +    my $l = shift;      my %options = @_; +    eval { +        die "Unspecified type\n" unless defined $l->{type}; +        die "Unknown type ‘".$l->{type}."’\n" +            unless grep { $l->{type} eq $_ } qw/user alias list/; +        my ($u, $d) = split_addr($l->{name}, -encode => 'ascii'); +        return unless $u && $d; +        # ^ To avoid unicode issues. +        die "Recipient delimiter ‘+’ is not allowed in locaparts\n" +                if $u =~ /\+/; # TODO: should be a config option +        $l->{name} = email_valid( $u.'@'.$d, -exact => 1 ); + +        unless ($options{'-append'} or $options{'-replace'}) { +            my @must = qw/name isActive/; +            push @must, $l->{type} eq 'user'  ? 'password' : +                        # TODO: ^ match 'quota' against the Dovecot specifications +                        $l->{type} eq 'alias' ? 'destination' : +                        $l->{type} eq 'list'  ? qw/transport password/ : +                        (); +            Fripost::Schema::Util::must_attrs( $l, @must ); +        } -    # We may not have read access to the list commands -    # The trick is somewhat dirty, but it's safe enough since postfix -    # delivers to users, aliases, and lists with different -    # priorities (and lists have the lowest). -    my @cmds = qw/admin bounces confirm join leave owner request -                  subscribe unsubscribe bounce sendkey/; -    my @tests = ( {fvu => $l}, {fva => $l}, {fvl => $l} ); - -    foreach (@cmds) { -        # If the entry is of the form 'foo-command', we need to ensure -        # that no list 'foo' exists, otherwise the new entry would -        # override foo's command. -        if ($l =~ s/-$_$//) { -            push @tests, {fvl => $l}; -            last; +        if ($l->{type} eq 'user') { +            $l->{forward} = [ map { email_valid($_) } @{$l->{forward}} ] +                if $l->{forward};          } -    } -    if (defined $options{t} and $options{t} eq 'list') { -        # If that's a list that is to be created, we need to ensure that -        # none of its commands exists. -        foreach (@cmds) { -            my $l2 = $l.'-'.$_; -            push @tests, {fvu => $l2}, {fva => $l2}; +        elsif ($l->{type} eq 'alias') { +            $a->{destination} = [ map { email_valid($_) } @{$l->{destination}} ] +                if $l->{destination};          } -    } +        elsif ($l->{type} eq 'list') { +            die "Invalid list name: ‘".$l->{name}."’\n" +                unless $u =~ /^[[:alnum:]_=\+\-\.]+$/; + +            die "Invalid list name: ‘".$l->{name}."’\n" +                if defined $l->{transport} and +                   grep {$u =~ /-$_$/} @{$list_commands{$l->{transport}}}; -    foreach (@tests) { -        my $dn = canonical_dn($_, {fvd => $d}, @{$self->suffix}); -        my $mesg = $self->ldap->search( base => $dn -                                      , scope => 'base' -                                      , deref => 'never' -                                      , filter => 'objectClass=*' -                                      , attrs => [ '1.1' ] -        ); -        return 1 unless $mesg->code; # 0  Success -        unless ($mesg->code == 32) { # 32 No such object -            die $options{'-die'}."\n" if defined $options{'-die'}; -            die $mesg->error."\n"; +            die "Invalid transport: ‘".$l->{transport}."’\n" +                if defined $l->{transport} and +                   not grep { $l->{transport} eq $_ } qw/schleuder mailman/; + +            $l->{transport} //= 'mailman' +                unless $options{'-append'} or $options{'-replace'};          } -    } -    return 0; +    }; +    softdie ($@, %options);  } -=back +sub _mkLocalAlias { +    my $name = email_to_ascii(shift); +    $name =~ /^(.+)@([^\@]+)/ or return; +    return $1.'#'.$2; +} + + + + -=head1 GLOBAL OPTIONS -If the B<-concat> option is present, it will intersperse multi-valued -attributes. Otherwise, an array reference containing every values will -be returned for these attributes. -The B<-die> option, if present, overides LDAP croaks and errors. -=cut  =head1 AUTHOR diff --git a/templates/add-alias.html b/templates/add-alias.html index 2844dad..1a7d7dd 100644 --- a/templates/add-alias.html +++ b/templates/add-alias.html @@ -2,17 +2,17 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Add alias under <TMPL_VAR NAME=domain></title> +    <title>Add alias under <TMPL_VAR NAME=domainname></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../">Root</a> / -        <a href="./"><TMPL_VAR NAME=domain></a> / +        <a href="./"><TMPL_VAR NAME=domainname></a> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../?a=logout">Log out</a>        </div> @@ -21,7 +21,7 @@      <hr/>      <div id="content"> -    <h1>Add alias under <span class="domain"><TMPL_VAR NAME=domain></span></h1> +    <h1>Add alias under <span class="domain"><TMPL_VAR NAME=domainname></span></h1>      <TMPL_IF NAME=error>        <div class="fail">Error: <TMPL_VAR NAME=error></div> @@ -37,14 +37,14 @@        <input type="hidden" name="t" value="alias" />        <h4 class="label">Alias name</h4> -      <input type="text" name="alias" size="15" value="<TMPL_VAR NAME=alias>"/>@<TMPL_VAR NAME=domain> +      <input type="text" name="name" size="15" value="<TMPL_VAR NAME=name>"/>@<TMPL_VAR NAME=domainname>        <hr/>        <h4 class="label">Status</h4> -      <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +      <select name="isActive"> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. @@ -61,7 +61,7 @@        <hr/>        <h4 class="label">Destination(s)</h4> -      <textarea name="maildrop" cols="50" rows="5" ><TMPL_VAR NAME=maildrop></textarea> +      <textarea name="destination" cols="50" rows="5" ><TMPL_VAR NAME=destination></textarea>        <div class="help">          The list of destinations (one e-mail address per line) that          will receive mail for this alias. diff --git a/templates/add-list.html b/templates/add-list.html index 136899d..6e9f8bd 100644 --- a/templates/add-list.html +++ b/templates/add-list.html @@ -2,17 +2,17 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Add list under <TMPL_VAR NAME=domain></title> +    <title>Add list under <TMPL_VAR NAME=domainname></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../">Root</a> / -        <a href="./"><TMPL_VAR NAME=domain></a> / +        <a href="./"><TMPL_VAR NAME=domainname></a> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../?a=logout">Log out</a>        </div> @@ -21,7 +21,7 @@      <hr/>      <div id="content"> -    <h1>Add list under <span class="domain"><TMPL_VAR NAME=domain></span></h1> +    <h1>Add list under <span class="domain"><TMPL_VAR NAME=domainname></span></h1>      <TMPL_IF NAME=error>        <div class="fail">Error: <TMPL_VAR NAME=error></div> @@ -37,14 +37,14 @@        <input type="hidden" name="t" value="list" />        <h4 class="label">List name</h4> -      <input type="text" name="list" size="15" value="<TMPL_VAR NAME=list>"/>@<TMPL_VAR NAME=domain> +      <input type="text" name="name" size="15" value="<TMPL_VAR NAME=name>"/>@<TMPL_VAR NAME=domainname>        <hr/>        <h4 class="label">Status</h4> -      <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +      <select name="isActive"> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. @@ -82,8 +82,9 @@        <h4 class="label">Transport</h4>        <select name="transport"> -        <option value="mailman" <TMPL_UNLESS NAME=isenc>selected="selected"</TMPL_UNLESS>>GNU Mailman</option> -        <option value="schleuder" <TMPL_IF NAME=isenc>selected="selected"</TMPL_IF>>Schleuder</option> +        <TMPL_LOOP NAME=transport> +          <option value="<TMPL_VAR NAME=item>" <TMPL_IF NAME=selected>selected="selected"</TMPL_IF>><TMPL_VAR NAME=name></option> +        </TMPL_LOOP>        </select>        <div class="help">          The mailing list manager. (<i>Note</i>: It is not possible to @@ -103,7 +104,7 @@          <i>Note</i>: No confirmation email will be sent. It may take a while for the list to be created (especially          for the Schleuder list manager, as it requires a GPG key creation); Once the list has succefully been created,          it will be visible under the -        <a href="./">management page for <span class="domain"><TMPL_VAR NAME=domain></span></a>. +        <a href="./">management page for <span class="domain"><TMPL_VAR NAME=domainname></span></a>.        </div>        </div>      </form> diff --git a/templates/add-user.html b/templates/add-user.html index 8964c22..67493a1 100644 --- a/templates/add-user.html +++ b/templates/add-user.html @@ -2,17 +2,17 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Add user under <TMPL_VAR NAME=domain></title> +    <title>Add user under <TMPL_VAR NAME=domainname></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../">Root</a> / -        <a href="./"><TMPL_VAR NAME=domain></a> / +        <a href="./"><TMPL_VAR NAME=domainname></a> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../?a=logout">Log out</a>        </div> @@ -21,7 +21,7 @@      <hr/>      <div id="content"> -    <h1>Add user under <span class="domain"><TMPL_VAR NAME=domain></span></h1> +    <h1>Add user under <span class="domain"><TMPL_VAR NAME=domainname></span></h1>      <TMPL_IF NAME=error>        <div class="fail">Error: <TMPL_VAR NAME=error></div> @@ -37,14 +37,14 @@        <input type="hidden" name="t" value="user" />        <h4 class="label">User name</h4> -      <input type="text" name="user" size="15" value="<TMPL_VAR NAME=user>"/>@<TMPL_VAR NAME=domain> +      <input type="text" name="name" size="15" value="<TMPL_VAR NAME=name>"/>@<TMPL_VAR NAME=domainname>        <hr/>        <h4 class="label">Status</h4> -      <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +      <select name="isActive"> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. @@ -76,7 +76,7 @@        <hr/>        <h4 class="label">Mail forwarding</h4> -      <textarea name="forwards" cols="50" rows="5" ><TMPL_VAR NAME=forwards></textarea> +      <textarea name="forward" cols="50" rows="5" ><TMPL_VAR NAME=forward></textarea>        <div class="help">          An optional list of destinations (one e-mail address per line) that          will <i>also</i> receive mail delivered to this user. diff --git a/templates/edit-alias.html b/templates/edit-alias.html index 34425a4..ba2f20d 100644 --- a/templates/edit-alias.html +++ b/templates/edit-alias.html @@ -2,18 +2,18 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Edit alias <TMPL_VAR NAME=alias>@<TMPL_VAR NAME=domain></title> +    <title>Edit alias <TMPL_VAR NAME=name></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../../">Root</a> / -        <a href="../"><TMPL_VAR NAME=domain></a> / -        <TMPL_VAR NAME=alias> / +        <a href="../"><TMPL_VAR NAME=domainpart></a> / +        <TMPL_VAR NAME=localpart> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../../?a=logout">Log out</a>        </div> @@ -22,7 +22,7 @@      <hr/>      <div id="content"> -    <h1>Edit alias <span class="email"><TMPL_VAR NAME=alias>@<TMPL_VAR NAME=domain></span><TMPL_IF NAME=canDelete +    <h1>Edit alias <span class="email"><TMPL_VAR NAME=name></span><TMPL_IF NAME=canDelete        ><span class="action">[<a href="./?a=delete">delete</a>]</span        ></TMPL_IF></h1> @@ -43,9 +43,9 @@        <input type="hidden" name="a" value="edit" />        <h4 class="label" id="status">Status</h4> -      <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +      <select name="isActive"> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. @@ -62,11 +62,11 @@        <hr/>        <h4 class="label" id="destination">Destination(s)</h4> -      <textarea name="maildrop" cols="50" rows="5" ><TMPL_VAR NAME=maildrop></textarea> +      <textarea name="destination" cols="50" rows="5" ><TMPL_VAR NAME=destination></textarea>        <div class="help">          The list of destinations (one e-mail address per line) that          will receive mail sent to -        <span class="email"><TMPL_VAR NAME=alias>@<TMPL_VAR NAME=domain></span>. +        <span class="email"><TMPL_VAR NAME=name></span>.        </div>        <hr/> diff --git a/templates/edit-list.html b/templates/edit-list.html index bb26eef..6930fd1 100644 --- a/templates/edit-list.html +++ b/templates/edit-list.html @@ -2,18 +2,18 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Edit list <TMPL_VAR NAME=list>@<TMPL_VAR NAME=domain></title> +    <title>Edit list <TMPL_VAR NAME=name></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../../">Root</a> / -        <a href="../"><TMPL_VAR NAME=domain></a> / -        <TMPL_VAR NAME=list> / +        <a href="../"><TMPL_VAR NAME=domainpart></a> / +        <TMPL_VAR NAME=localpart> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../../?a=logout">Log out</a>        </div> @@ -22,8 +22,8 @@      <hr/>      <div id="content"> -    <h1>Edit list <a class="external" target="_blank" href="<TMPL_VAR NAME=listURL>" -                  ><span class="email"><TMPL_VAR NAME=list>@<TMPL_VAR NAME=domain></span +    <h1>Edit list <a class="external" target="_blank" href="<TMPL_VAR NAME=list_URL>" +                  ><span class="email"><TMPL_VAR NAME=name></span                    ></a></h1>      <TMPL_IF NAME=newChanges> @@ -44,8 +44,8 @@        <h4 class="label" id="status">Status</h4>        <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. diff --git a/templates/edit-user.html b/templates/edit-user.html index 38d3836..d4b3c86 100644 --- a/templates/edit-user.html +++ b/templates/edit-user.html @@ -2,18 +2,18 @@  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">    <head>      <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> -    <title>Edit user <TMPL_VAR NAME=user>@<TMPL_VAR NAME=domain></title> +    <title>Edit user <TMPL_VAR NAME=name></title>      <link href="/css/style.css" media="all" rel="stylesheet" type="text/css" />    </head>    <body>      <div id="header">        <div class="left column">          <a href="../../">Root</a> / -        <a href="../"><TMPL_VAR NAME=domain></a> / -        <TMPL_VAR NAME=user> / +        <a href="../"><TMPL_VAR NAME=domainpart></a> / +        <TMPL_VAR NAME=localpart> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../../?a=logout">Log out</a>        </div> @@ -22,7 +22,7 @@      <hr/>      <div id="content"> -    <h1>Edit user <span class="user"><TMPL_VAR NAME=user>@<TMPL_VAR NAME=domain></span></h1> +    <h1>Edit user <span class="user"><TMPL_VAR NAME=name></span></h1>      <TMPL_IF NAME=newChanges> @@ -42,9 +42,9 @@        <input type="hidden" name="a" value="edit" />        <h4 class="label" id="status">Status</h4> -      <select name="isactive"> -        <option value="1" <TMPL_IF NAME=isactive>selected="selected"</TMPL_IF>>Active</option> -        <option value="0" <TMPL_UNLESS NAME=isactive>selected="selected"</TMPL_UNLESS>>Inactive</option> +      <select name="isActive"> +        <option value="1" <TMPL_IF NAME=isActive>selected="selected"</TMPL_IF>>Active</option> +        <option value="0" <TMPL_UNLESS NAME=isActive>selected="selected"</TMPL_UNLESS>>Inactive</option>        </select>        <div class="help">          <b>Warning</b>: emails are <i>not</i> delivered to inactive entries. @@ -89,14 +89,14 @@        <hr/>        <h4 class="label" id="forward">Mail forwarding</h4> -      <textarea name="forwards" cols="50" rows="5" ><TMPL_VAR NAME=forwards></textarea> +      <textarea name="forward" cols="50" rows="5" ><TMPL_VAR NAME=forward></textarea>        <div class="help">          An optional list of destinations (one e-mail address per line) that          will receive mail for -        <span class="email"><TMPL_VAR NAME=user>@<TMPL_VAR NAME=domain></span>. +        <span class="email"><TMPL_VAR NAME=name></span>.          (<i>Note</i>: When not empty, this list cancels delivery to          this user, so do not forget to list -        <span class="email"><TMPL_VAR NAME=user>@<TMPL_VAR NAME=domain></span> +        <span class="email"><TMPL_VAR NAME=name></span>          here as well if you want this mailbox to be delivered too.)        </div> @@ -262,9 +262,9 @@              the recipient(s), depending on how the message has been              classified.              (Adding the extension <span class="email">virus</span> to -            the recipient <span class="email"><TMPL_VAR NAME=user>@<TMPL_VAR NAME=domain></span> +            the recipient <span class="email"><TMPL_VAR NAME=name></span>              will result into viruses being sent to -            <span class="email"><TMPL_VAR NAME=user>+virus@<TMPL_VAR NAME=domain></span>.)</td> +            <span class="email"><TMPL_VAR NAME=localpart>+virus@<TMPL_VAR NAME=domainpart></span>.)</td>            <td><input type="text" name="amavisAddrExtensionVirus" size="10" /> Virus</td>          </tr>          <tr> diff --git a/templates/list-locals.html b/templates/list-locals.html index 44ef0be..e677dd8 100644 --- a/templates/list-locals.html +++ b/templates/list-locals.html @@ -11,7 +11,7 @@          <a href="../">Root</a> / <TMPL_VAR NAME=name> /        </div>        <div class="right column"> -        Logged as <a href="<TMPL_VAR NAME=userURL>/?a=edit" +        Logged as <a href="<TMPL_VAR NAME=user_URL>/?a=edit"                    ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a>          | <a href="../?a=logout">Log out</a>        </div> @@ -130,7 +130,7 @@          <TMPL_IF NAME=__even__><tr class="odd"><TMPL_ELSE><tr></TMPL_IF>            <td><span class="list"><TMPL_UNLESS NAME=isPending><a href="<TMPL_VAR NAME=URL>/"></TMPL_UNLESS               ><TMPL_VAR NAME=name -             ><TMPL_UNLESS NAME=isPending></a> <a class="external" target="_blank" href="<TMPL_VAR NAME=listURL>">➠</a></TMPL_UNLESS></span></td> +             ><TMPL_UNLESS NAME=isPending></a> <a class="external" target="_blank" href="<TMPL_VAR NAME=list_URL>">➠</a></TMPL_UNLESS></span></td>            <td><TMPL_IF NAME=description><TMPL_VAR NAME=description><TMPL_ELSE><span class="none">(none)</span></TMPL_IF></td>            <td><TMPL_IF NAME=isPending><span class="pending">⚑</span>                <TMPL_ELSE><TMPL_IF NAME=isActive><span class="active">✔</span>  | 
