diff options
-rw-r--r-- | css/style.css | 57 | ||||
-rw-r--r-- | lib/Fripost/Panel/Interface.pm | 157 | ||||
-rw-r--r-- | lib/Fripost/Panel/Login.pm | 9 | ||||
-rw-r--r-- | lib/Fripost/Schema/Domain.pm | 164 | ||||
-rw-r--r-- | lib/Fripost/Schema/List.pm | 7 | ||||
-rw-r--r-- | lib/Fripost/Schema/Local.pm | 9 | ||||
-rw-r--r-- | lib/Fripost/Schema/User.pm | 54 | ||||
-rw-r--r-- | lib/Fripost/Schema/Util.pm | 5 | ||||
-rw-r--r-- | templates/add-domain-1.html | 91 | ||||
-rw-r--r-- | templates/add-domain-2.html | 63 | ||||
-rw-r--r-- | templates/list-domains.html | 15 | ||||
-rw-r--r-- | templates/list-locals.html | 2 | ||||
-rw-r--r-- | templates/new-domain.tt | 32 |
13 files changed, 576 insertions, 89 deletions
diff --git a/css/style.css b/css/style.css index a9090e5..633ab09 100644 --- a/css/style.css +++ b/css/style.css @@ -7,16 +7,30 @@ body { width: 600pt; margin: 5pt auto; } -.error { +p.error { color: #FF0040; } -.inactive { +div.error { + margin: 20pt auto 15pt auto; + border: 2pt solid #FF5C5C; + padding: 5pt; + width: 50%; + background: #FFDFDF; +} +div.message { + margin: 20pt auto 15pt auto; + border: 1px solid #cccccc; + padding: 5pt; + width: 50%; + background:#E7EFF7; +} +span.inactive { color: #FF0040; } -.active { +span.active { color: #32CD32; } -.pending { +span.pending { color: #F77C13; } h1, h2, h3 { @@ -141,6 +155,21 @@ table.list thead th { text-align: center; color:#66a3d3 } +table.align { + border:1px solid #e5eff8; + margin:1em auto; + border-collapse:collapse; + margin: 0 auto; +} +table.align td { + color:#678197; + border: 0; + padding:.3em 1em; + text-align:center; +} +table.align tr.odd td { + background:#f7fbff +} .permlist { font-size: 9pt; font-weight: normal; @@ -153,6 +182,8 @@ font-weight: normal; font-weight: bolder; color: #888888; } +a.dunno { text-decoration: none; } +a.dunno:hover { color: purple} .action { font-weight: normal; font-size: 11pt; @@ -198,3 +229,21 @@ div.editform { color: #FF0040; text-align: center; } +#content form { + text-align: center; +} +input.hidden { + border: 0 none; + height: 0; + width: 0; + padding: 0; + margin: 0; + overflow: hidden; +} +form.unlock { + padding: 0pt; +} +form.unlock input { + vertical-align: middle; + font-size: 8pt; +} diff --git a/lib/Fripost/Panel/Interface.pm b/lib/Fripost/Panel/Interface.pm index d04fa3a..e4724d1 100644 --- a/lib/Fripost/Panel/Interface.pm +++ b/lib/Fripost/Panel/Interface.pm @@ -18,7 +18,8 @@ use Fripost::Schema::Util 'split_addr'; use Fripost::Password; use HTML::Entities 'encode_entities'; use URI::Escape::XS 'encodeURIComponent'; -use Net::IDN::Encode qw/email_to_unicode email_to_ascii/; +use Net::IDN::Encode qw/email_to_unicode email_to_ascii domain_to_ascii/; +use Encode; # This method is called right before the 'setup' method below. It @@ -38,8 +39,6 @@ sub ListDomains : StartRunmode { my $self = shift; my %CFG = $self->cfg; - my ($ul,$ud) = split_addr( $self->authen->username, -encode => 'unicode' ); - my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); my @domains = $fp->domain->search( -concat => "\n", -die => 403); $fp->done; @@ -49,6 +48,7 @@ sub ListDomains : StartRunmode { $template->param( $self->userInfo ); $template->param( domains => [ map { { &mkLink( domain => $_->{domain}) , isactive => $_->{isactive} + , ispending => $_->{ispending} , description => $_->{description} } } @domains ] ); @@ -62,10 +62,16 @@ sub ListLocals : Runmode { my $self = shift; my %CFG = $self->cfg; - my ($ul,$ud) = split_addr( $self->authen->username, -encode => 'unicode' ); my $d = ($self->split_path)[1]; my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); + my $q = $self->query; + if (defined $q->param('unlock') and $q->param('unlock') ne '') { + $fp->domain->unlock( $d, $q->param('unlock') ); + $fp->done; + return $self->redirect('../'); + } + # Query *the* matching domain my %domain = $fp->domain->get( $d, -die => 404 ); @@ -156,7 +162,6 @@ sub EditDomain : Runmode { my $self = shift; my %CFG = $self->cfg; - my ($ul,$ud) = split_addr( $self->authen->username, -encode => 'unicode' ); my $d = ($self->split_path)[1]; my $q = $self->query; @@ -169,11 +174,11 @@ sub EditDomain : Runmode { # Changes have been submitted: process them $error = $fp->domain->replace({ domain => $d, - isactive => $q->param('isactive'), - description => $q->param('description'), - catchalls => $q->param('catchalls'), - canAddAlias => $q->param('canAddAlias'), - canAddList => $q->param('canAddList') + isactive => $q->param('isactive') // 1, + description => $q->param('description') // undef, + catchalls => $q->param('catchalls') // undef, + canAddAlias => $q->param('canAddAlias') // undef, + canAddList => $q->param('canAddList') // undef }, -concat => "(\n|\x{0D}\x{0A})"); } my %domain = $fp->domain->get( $d, -die => 404 ); @@ -186,11 +191,11 @@ sub EditDomain : Runmode { , isPostmaster => $domain{permissions} eq 'p'); if ($error) { # Preserve the (incorrect) form - $template->param( isactive => $q->param('isactive') - , description => $q->param('description') - , catchalls => $q->param('catchalls') - , canAddAlias => $q->param('canAddAlias') - , canAddList => $q->param('canAddList') + $template->param( isactive => $q->param('isactive') // 1 + , description => $q->param('description') // undef + , catchalls => $q->param('catchalls') // undef + , canAddAlias => $q->param('canAddAlias') // undef + , canAddList => $q->param('canAddList') // undef , error => encode_entities ($error) ); } else { @@ -241,11 +246,11 @@ sub EditLocal : Runmode { my %entry; if ($t eq 'user') { $entry{user} = $l.'@'.$d; - $entry{forwards} = $q->param('forwards'); + $entry{forwards} = $q->param('forwards') // undef; - if ($q->param('oldpw') ne '' or - $q->param('newpw') ne '' or - $q->param('newpw2') ne '') { + 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. @@ -263,13 +268,13 @@ sub EditLocal : Runmode { my $u = email_to_unicode($self->authen->username); $fp = Fripost::Schema::->auth( $u, - $q->param('oldpw'), + $q->param('oldpw') // '', %CFG, -die => "Wrong password (for ‘".$u."’)." ); }; $error = $@ || $fp->user->passwd( $entry{user}, - Fripost::Password::hash($q->param('newpw')) + Fripost::Password::hash($q->param('newpw') // '') ); $fp->done if defined $fp; } @@ -277,14 +282,14 @@ sub EditLocal : Runmode { } elsif ($t eq 'alias') { $entry{alias} = $l.'@'.$d; - $entry{maildrop} = $q->param('maildrop'); + $entry{maildrop} = $q->param('maildrop') // undef; } elsif ($t eq 'list') { $entry{list} = $l.'@'.$d; - $entry{transport} = $q->param('transport'); + $entry{transport} = $q->param('transport') // undef; } - $entry{isactive} = $q->param('isactive'); - $entry{description} = $q->param('description'); + $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; } @@ -297,17 +302,17 @@ sub EditLocal : Runmode { # Preserve the (incorrect) form, except the passwords if ($t eq 'user') { $template->param( user => encode_entities($l) - , forwards => $q->param('forwards') ); + , forwards => $q->param('forwards') // undef ); } elsif ($t eq 'alias') { $template->param( alias => encode_entities($l) - , maildrop => $q->param('maildrop') ); + , maildrop => $q->param('maildrop') // undef ); } elsif ($t eq 'list') { $template->param( list => encode_entities($l) ); } - $template->param( isactive => $q->param('isactive') - , description => $q->param('description') ); + $template->param( isactive => $q->param('isactive') // 1 + , description => $q->param('description') // undef ); } else { %local = $fp->local->get ($l.'@'.$d, -die => 404, @@ -339,6 +344,82 @@ sub EditLocal : Runmode { return $template->output; } +sub AddDomain : Runmode { + my $self = shift; + my %CFG = $self->cfg; + + my $q = $self->query; + return $self->redirect('./') if defined $q->param('cancel'); # Cancellation + + my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); + my $domain = $q->param('domain'); + Encode::_utf8_on($domain) if defined $domain; + my $session_param; + $session_param = 'AddDomain-Postmasters-' . domain_to_ascii($domain) + if defined $domain; + + my $error; # Tells whether the change submission has failed. + if (defined $q->param('submit')) { + # Changes have been submitted: process them + + if (defined $q->param('postmaster') and defined $session_param) { + my @postmasters = split /\s*,\s*/, $self->session->param($session_param); + $error = "‘".$q->param('postmaster')."’ was not listed among the domain owners." + unless defined $self->session->param($session_param) + and grep { $q->param('postmaster') eq $_ } @postmasters; + } + + $error = $fp->domain->add({ + domain => $domain, + send_token_to => $q->param('postmaster') // undef, + isactive => $q->param('isactive') // 1, + description => $q->param('description') // undef, + catchalls => $q->param('catchalls') // undef }, + -concat => "(\n|\x{0D}\x{0A})", + '-dry-run' => not (defined $q->param('postmaster')), + -domainurl => $q->url.'/'.encode_entities($domain).'/' # TODO: try that in nginx + ) unless $error; + } + $fp->done; + + return $self->redirect('./') # Confirmation token sent, everything OK + if ($error // '') eq '' and defined $q->param('postmaster'); + + my $tmpl_file; + my @postmasters; + + if (($error // '') ne '' or not (defined $domain)) { + # Something went wrong, or the domain is unknown + $tmpl_file = 'add-domain-1.html'; + } + else { + $tmpl_file = 'add-domain-2.html'; + @postmasters = Fripost::Schema::Domain::->list_postmasters($domain); + } + + my $template = $self->load_tmpl( $tmpl_file, cache => 1, + , loop_context_vars => 1 ); + $template->param( $self->userInfo ); + $template->param( error => encode_entities ($error) ) if $error; + + $template->param( isactive => $q->param('isactive') // 1 + , description => $q->param('description') // undef + , catchalls => $q->param('catchalls') // undef + ); + $template->param( domain => encode_entities($domain) ) + if defined $domain; + + if (@postmasters) { + # Store it, to ensure the user doesn't send back a bogus email + $self->session->param( $session_param, join(',', @postmasters) ); + $self->session->flush; + + $template->param( postmasters => [ map {{postmaster => $_}} @postmasters ] ) + } + + return $template->output; +} + # In this Run Mode authenticated users can add users, aliases and lists # (if they have the permission). @@ -400,8 +481,8 @@ sub AddLocal : Runmode { # Unknown type return $self->redirect('./'); } - $entry{isactive} = $q->param('isactive'); - $entry{description} = $q->param('description'); + $entry{isactive} = $q->param('isactive') // 1; + $entry{description} = $q->param('description') // undef; unless ($error) { my $fp = Fripost::Schema::->SASLauth( $self->authen->username, %CFG ); @@ -417,23 +498,23 @@ sub AddLocal : Runmode { if ($error) { # Preserve the (incorrect) form, except the passwords if ($t eq 'user') { - $template->param( user => $q->param('user') - , forwards => $q->param('forwards') ); + $template->param( user => $q->param('user') // undef + , forwards => $q->param('forwards') // undef ); } elsif ($t eq 'alias') { - $template->param( alias => $q->param('alias') - , maildrop => $q->param('maildrop') ); + $template->param( alias => $q->param('alias') // undef + , maildrop => $q->param('maildrop') // undef ); } elsif ($t eq 'list') { - $template->param( list => $q->param('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') - , description => $q->param('description') + $template->param( isactive => $q->param('isactive') // 1 + , description => $q->param('description') // undef , error => encode_entities ($error) ); } else { diff --git a/lib/Fripost/Panel/Login.pm b/lib/Fripost/Panel/Login.pm index 12b0294..b0906b3 100644 --- a/lib/Fripost/Panel/Login.pm +++ b/lib/Fripost/Panel/Login.pm @@ -105,13 +105,12 @@ sub setup { # /domain/{user,alias,list}/?query_url my ($null,$domain,$local,$crap) = $self->split_path; - return 'ListDomains' unless (defined $null) and $null eq ''; + return 'ListDomains' if (defined $null) and $null ne ''; unless (defined $domain and $domain ne '') { - # TODO -# if (defined $a) { -# return 'AddDomain' if $a eq 'add'; -# } + if (defined $a) { + return 'AddDomain' if $a eq 'add'; + } return 'ListDomains'; } diff --git a/lib/Fripost/Schema/Domain.pm b/lib/Fripost/Schema/Domain.pm index e86822f..993d771 100644 --- a/lib/Fripost/Schema/Domain.pm +++ b/lib/Fripost/Schema/Domain.pm @@ -21,6 +21,13 @@ use Fripost::Schema::Util qw/concat get_perms explode must_attrs email_valid canonical_dn/; use Net::IDN::Encode qw/domain_to_ascii domain_to_unicode email_to_ascii email_to_unicode/; +use Encode; +use Net::Domain::TLD 'tld_exists'; +use Net::DNS::Dig; +use Net::Whois::Parser 'parse_whois'; +use String::Random; +use Template; +use MIME::Entity; =head1 METHODS @@ -44,7 +51,8 @@ sub search { scope => 'one', deref => 'never', filter => 'objectClass=FripostVirtualDomain', - attrs => [ qw/fvd description fripostIsStatusActive/ ] + attrs => [ qw/fvd description fripostIsStatusActive + fripostIsStatusPending/ ] ); if ($domains->code) { die $options{'-die'}."\n" if defined $options{'-die'}; @@ -52,6 +60,7 @@ sub search { } return map { { domain => domain_to_unicode($_->get_value('fvd')) , isactive => $_->get_value('fripostIsStatusActive') eq 'TRUE' + , ispending => defined $_->get_value('fripostIsStatusPending') , description => concat($concat, $_->get_value('description')) } } @@ -148,10 +157,158 @@ sub replace { } +sub list_postmasters { + my $self = shift; + my $hostname = shift; + my @postmasters; + + my $tld = domain_to_ascii($hostname); + my $domain; + until ( tld_exists($tld) ) { + die "‘".$hostname."’ has an Invalid TLD.\n" unless $tld =~ /\./; + $domain = $tld; + + my %mx = Net::DNS::Dig->new()->for( $domain, 'MX' )->rdata; + push @postmasters, 'postmaster@'.$domain # RFC 822, appendix C.6 + if grep {!/\bfripost\.org$/} (values %mx); + + $tld =~ s/^[^\.]*\.//; + } + + my $info = parse_whois( domain => $domain ); +# die "Cannot WHOIS ‘".$domain."’.\n" unless defined $info; + # TODO: there is a bug with the encoding + if (defined $info) { + push @postmasters, @{$info->{emails}}; + } + + my %hash; + $hash{$_} = 1 for grep {email_valid($_ // '', -nodie => 1 )} + @postmasters; + sort keys %hash; +} + sub add { - die "TODO"; + my $self = shift; + my $d = shift; + my %options = @_; + + foreach (qw/description catchalls canAddAlias canAddList/) { + $d->{$_} = explode ($options{'-concat'}, $d->{$_}) + if defined $d->{$_}; + } + + eval { + my $domain = $d->{domain}; + Encode::_utf8_on($domain); + &_is_valid($d); + + my $dn = canonical_dn( {fvd => $d->{domain}}, @{$self->suffix} ); + + my $mesg = $self->ldap->search( + base => $dn, + scope => 'base', + deref => 'never', + filter => 'objectClass=FripostVirtualDomain', + attrs => [] ); + if ($mesg->code == 0) { + die "Domain ‘".$domain."’ already exists.\n"; + } + elsif ($mesg->code != 32) { + die $mesg->error."\n"; + } + + return if $options{'-dry-run'}; + + my %attrs = ( objectClass => 'FripostVirtualDomain' + , fripostIsStatusActive => $d->{isactive} ? + 'TRUE' : 'FALSE' + ); + $attrs{description} = $d->{description} + if defined $d->{description} and @{$d->{description}}; + $attrs{fripostOptionalMaildrop} = $d->{catchalls} + if defined $d->{catchalls} and @{$d->{catchalls}}; + + if (defined $d->{owner}) { + $attrs{fripostOwner} = $self->_fvu2dn($d->{owner}) + if $d->{owner} ne ''; + } + else { + $attrs{fripostOwner} = $self->whoami; + } + + my $token; + if (defined $d->{send_token_to}) { + $token = String::Random::->new->randregex('\w{32}'); + $attrs{fripostIsStatusPending} = $token + } + + $mesg = $self->ldap->add( $dn, attrs => [ %attrs ] ); + die $mesg->error."\n" if $mesg->code; + + + if (defined $d->{send_token_to}) { + my $tt = Template->new({ + INCLUDE_PATH => './templates', # TODO: use a config option + INTERPOLATE => 1, + }) or die $Template::ERROR."\n"; + + my $data; + my $vars = { domain => $domain, token => $token }; + $vars->{unlockurl} = $options{'-domainurl'}.'?unlock='.$token + if defined $options{'-domainurl'}; + $tt->process( 'new-domain.tt', $vars, \$data) + or die $tt->error."\n"; + + my $mail = MIME::Entity::->build( + From => 'Fripost Admin Panel <AdminWebPanel@fripost.org>', + To => $d->{send_token_to}, + Subject => "Your new domain ".$d->{domain}, + Encoding => 'quoted-printable', + Charset => 'utf-8', + Data => $data + ); + $mail->send; + } + + }; + return $@; +} + +sub unlock { + my $self = shift; + my $d = shift; + my $code = shift; + + eval { + my $dn = canonical_dn({fvd => domain_to_ascii($d)}, @{$self->suffix}); + my $domains = $self->ldap->search( + base => $dn, + scope => 'base', + deref => 'never', + filter => '(&(objectClass=FripostVirtualDomain)'. + '(fripostIsStatusPending=*))', + attrs => [ 'fripostIsStatusPending' ] + ); + die $domains->error."\n" if $domains->code; + + my $domain = $domains->pop_entry; + die "No such such domain: ‘".$d."’\n" unless defined $domain; + + die "Wrong unlock code for ‘".$d."’\n" + unless $domain->get_value('fripostIsStatusPending') eq $code; + # TODO: a more secure option would be to add a 'userPassword' + # attribute to domains. We can bind as the domain to check the + # validity of the token and add an ACL rule to give =z rights on + # self and =0 for everyone else. + + my $mesg = $self->ldap->modify( $dn, delete => 'fripostIsStatusPending' ); + die $mesg->error."\n" if $mesg->code; + }; + return $@; } + =back =head1 GLOBAL OPTIONS @@ -179,6 +336,9 @@ sub _is_valid { @{$d->{canAddAlias}} ]; $d->{canAddList} = [ map { email_valid($_, -prefix => 'fake') } @{$d->{canAddList}} ]; + + $d->{send_token_to} = email_valid( $d->{send_token_to} ) + if defined $d->{send_token_to}; } diff --git a/lib/Fripost/Schema/List.pm b/lib/Fripost/Schema/List.pm index 18eeb29..2c4d1bc 100644 --- a/lib/Fripost/Schema/List.pm +++ b/lib/Fripost/Schema/List.pm @@ -42,7 +42,7 @@ sub search { my $concat = $options{'-concat'}; my $filter = 'objectClass=FripostVirtualList'; - $filter = '(&('.$filter.')(!(fripostIsStatusPending=TRUE)))' + $filter = '(&('.$filter.')(!(fripostIsStatusPending=*)))' if (defined $options{'-is_pending'}) and !$options{'-is_pending'}; my $lists = $self->ldap->search( @@ -62,7 +62,7 @@ sub search { , isactive => $_->get_value('fripostIsStatusActive') eq 'TRUE' , description => concat($concat, $_->get_value('description')) , transport => $_->get_value('fripostListManager') - , ispending => ($_->get_value('fripostIsStatusPending') // '') eq 'TRUE' + , ispending => defined $_->get_value('fripostIsStatusPending') } } $lists->sorted('fvl') @@ -144,8 +144,7 @@ sub add { # Ask the list manager to create the list now. my $member = email_valid( $self->_dn2fvu($self->whoami), -exact => 1); - my $to = email_valid( 'mklist+'.$l->{transport}.'@fripost.org' - , -exact => 1 ); + my $to = email_valid( 'mklist+'.$l->{transport}.'@fripost.org' ); my $mail = MIME::Entity::->build( From => 'Fripost Admin Panel <AdminWebPanel@fripost.org>', diff --git a/lib/Fripost/Schema/Local.pm b/lib/Fripost/Schema/Local.pm index 5ca833c..c342f23 100644 --- a/lib/Fripost/Schema/Local.pm +++ b/lib/Fripost/Schema/Local.pm @@ -141,10 +141,11 @@ sub exists { foreach (@tests) { my $dn = canonical_dn($_, {fvd => $d}, @{$self->suffix}); - my $mesg = $self->ldap->search( base => $dn, - scope => 'base', - deref => 'never', - filter => 'objectClass=*' + my $mesg = $self->ldap->search( base => $dn + , scope => 'base' + , deref => 'never' + , filter => 'objectClass=*' + , attrs => [] ); return 1 unless $mesg->code; # 0 Success unless ($mesg->code == 32) { # 32 No such object diff --git a/lib/Fripost/Schema/User.pm b/lib/Fripost/Schema/User.pm index 7d79e69..3b5cfca 100644 --- a/lib/Fripost/Schema/User.pm +++ b/lib/Fripost/Schema/User.pm @@ -72,23 +72,23 @@ Replace an existing account with the given one. sub replace { my $self = shift; - my $m = shift; + my $u = shift; my %options = @_; foreach (qw/description forwards/) { - $m->{$_} = explode ($options{'-concat'}, $m->{$_}) - if defined $m->{$_}; + $u->{$_} = explode ($options{'-concat'}, $u->{$_}) + if defined $u->{$_}; } eval { - my ($l,$d) = split_addr( $m->{user}, -encode => 'ascii' ); - &_is_valid($m); + my ($l,$d) = split_addr( $u->{user}, -encode => 'ascii' ); + &_is_valid($u); my $mesg = $self->ldap->modify( canonical_dn( {fvu => $l}, {fvd => $d}, @{$self->suffix} ), - replace => { fripostIsStatusActive => $m->{isactive} ? + replace => { fripostIsStatusActive => $u->{isactive} ? 'TRUE' : 'FALSE' - , description => $m->{description} - , fripostOptionalMaildrop => $m->{forwards} + , description => $u->{description} + , fripostOptionalMaildrop => $u->{forwards} } ); die $mesg->error."\n" if $mesg->code; }; @@ -126,30 +126,30 @@ Add the given account. sub add { my $self = shift; - my $m = shift; + my $u = shift; my %options = @_; foreach (qw/description forwards/) { - $m->{$_} = explode ($options{'-concat'}, $m->{$_}) - if defined $m->{$_}; + $u->{$_} = explode ($options{'-concat'}, $u->{$_}) + if defined $u->{$_}; } eval { - die "Missing user name\n" unless $m->{user} =~ /^.+\@.+$/; - my ($l,$d) = split_addr( $m->{user}, -encode => 'ascii' ); - &_is_valid($m); - die "‘".$m->{user}."’ already exists\n" - if $self->local->exists($m->{user},%options); + die "Missing user name\n" unless $u->{user} =~ /^.+\@.+$/; + my ($l,$d) = split_addr( $u->{user}, -encode => 'ascii' ); + &_is_valid($u); + die "‘".$u->{user}."’ already exists\n" + if $self->local->exists($u->{user},%options); my %attrs = ( objectClass => 'FripostVirtualUser' - , fripostIsStatusActive => $m->{isactive} ? 'TRUE' : 'FALSE' - , userPassword => $m->{password} + , fripostIsStatusActive => $u->{isactive} ? 'TRUE' : 'FALSE' + , userPassword => $u->{password} ); - $attrs{description} = $m->{description} - if defined $m->{description} and @{$m->{description}}; - $attrs{fripostUserQuota} = $m->{quota} if defined $m->{quota}; - $attrs{fripostOptionalMaildrop} = $m->{forwards} - if defined $m->{forwards} and @{$m->{forwards}}; + $attrs{description} = $u->{description} + if defined $u->{description} and @{$u->{description}}; + $attrs{fripostUserQuota} = $u->{quota} if defined $u->{quota}; + $attrs{fripostOptionalMaildrop} = $u->{forwards} + if defined $u->{forwards} and @{$u->{forwards}}; my $mesg = $self->ldap->add( canonical_dn( {fvu => $l}, {fvd => $d}, @{$self->suffix} ), @@ -203,10 +203,10 @@ The B<-die> option, if present, overides LDAP croaks and errors. # Ensure that the given user is valid. sub _is_valid { - my $m = shift; - must_attrs( $m, qw/user isactive/ ); - $m->{user} = email_valid( $m->{user}, -exact => 1); - $m->{forwards} = [ map { email_valid($_) } @{$m->{forwards}} ]; + my $u = shift; + must_attrs( $u, qw/user isactive/ ); + $u->{user} = email_valid( $u->{user}, -exact => 1); + $u->{forwards} = [ map { email_valid($_) } @{$u->{forwards}} ]; # TODO: match 'quota' against the Dovecot specifications } diff --git a/lib/Fripost/Schema/Util.pm b/lib/Fripost/Schema/Util.pm index b26214b..2692421 100644 --- a/lib/Fripost/Schema/Util.pm +++ b/lib/Fripost/Schema/Util.pm @@ -116,7 +116,10 @@ sub email_valid { -fqdn => 1 ); my $match = defined $addr; $match &&= $addr eq $in if $options{'-exact'}; - die $mesg." ‘".$i."’\n" unless $match; + unless ($match) { + return if $options{'-nodie'}; + die $mesg." ‘".$i."’\n"; + } $addr =~ s/^$options{'-prefix'}// if defined $options{'-prefix'}; return $addr; } diff --git a/templates/add-domain-1.html b/templates/add-domain-1.html new file mode 100644 index 0000000..df6ac65 --- /dev/null +++ b/templates/add-domain-1.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<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 domain</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> / + </div> + <div class="right column"> + Logged as <a href="<TMPL_VAR NAME=userURI>/?a=edit" + ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a> + | <a href="./?a=logout">Log out</a> + </div> + <br/> + </div> + <hr/> + + <div id="content"> + <h1>Add domain</h1> + + <TMPL_IF NAME=error> + <div class="fail">Error: <TMPL_VAR NAME=error></div> + <TMPL_ELSE> + <br/> + </TMPL_IF> + + <br/> + + <form class="editform" name="editform" method="post" action="?"> + <div class="editform"> + <input type="hidden" name="a" value="add" /> + + <h4 class="label" id="domain">Domain name</h4> + <input type="text" name="domain" size="15" value="<TMPL_VAR NAME=domain>" /> + <div class="help"> + The domain name to be added. + + If you want to transfer an existing domain do use our MX'es, we + will ask you to an e-mail where to send a confirmation token + token proving you indeed own the domain. + + Buying new domains from this interface will be feasible in the + future, but it has not been implemented yet. + </div> + + <hr/> + + <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> + <div class="help"> + <b>Warning</b>: emails are <i>not</i> delivered to users, + aliases or lists under inactive domains. + </div> + + <hr/> + + <h4 class="label" id="description">Description</h4> + <textarea name="description" cols="50" rows="5" ><TMPL_VAR NAME=description></textarea> + <div class="help"> + An optional description. (HTML tags are allowed.) + </div> + + <hr/> + + <h4 class="label" id="catch-all">Catch-All aliases</h4> + <textarea name="catchalls" cols="50" rows="5" ><TMPL_VAR NAME=catchalls></textarea> + <div class="help"> + An optional list of destinations (one e-mail address per line) that + will receive mail sent to <i>non existing</i> recipients. + Domain aliases can be defined by leaving the local part of + the destination empty, like in <span class="email">@example.org</span>: + email to <span class="email">inexisting@this-new-domain.org</span> + will then be sent to <span class="email">inexisting@example.org</span>. + </div> + <hr/> + <br/> + + <input type="submit" name="submit" value="Submit" /> + <input type="submit" name="cancel" value="Cancel" /> + </div> + </form> + </div> + </body> +</html> diff --git a/templates/add-domain-2.html b/templates/add-domain-2.html new file mode 100644 index 0000000..0e1b660 --- /dev/null +++ b/templates/add-domain-2.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<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 domain <TMPL_VAR NAME=domain></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> / + </div> + <div class="right column"> + Logged as <a href="<TMPL_VAR NAME=userURI>/?a=edit" + ><TMPL_VAR NAME=user_localpart>@<TMPL_VAR NAME=user_domainpart></a> + | <a href="./?a=logout">Log out</a> + </div> + <br/> + </div> + <hr/> + + <div id="content"> + <form name="addform" method="post" action="?"> + <input type="hidden" name="a" value="add" /> + <input type="hidden" name="domain" value="<TMPL_VAR NAME=domain>" /> + <input type="hidden" name="isactive" value="<TMPL_VAR NAME=isactive>" /> + <input type="hidden" name="description" value="<TMPL_VAR NAME=description>" /> + <input type="hidden" name="catchalls" value="<TMPL_VAR NAME=catchalls>" /> + + <TMPL_IF NAME=postmasters> + <div class="message"> + You have asked to let us manage e-mail + <span class="email">@<TMPL_VAR NAME=domain></span>. + Please select an address below where to send a confirmation + token, proving that you indeed own the domain + <span class="domain"><TMPL_VAR NAME=domain></span>. + </div> + <table class="align" id="postmasters"> + <tbody> + <TMPL_LOOP NAME=postmasters> + <TMPL_IF NAME=__even__><tr class="odd"><TMPL_ELSE><tr></TMPL_IF> + <td><input type="radio" name="postmaster" value="<TMPL_VAR NAME=postmaster>"/></td> + <td><span class="email"><TMPL_VAR NAME=postmaster></td> + </tr> + </TMPL_LOOP> + </tbody> + </table> + <input type="submit" name="submit" value="1" class="hidden" /> + <input type="submit" name="cancel" value="Cancel" /> + <input type="submit" name="submit" value="Send" /> + <TMPL_ELSE> + <div class="error"> + I couldn't find a valid e-mail in your domain's WHOIS, + and no valid postmaster address has been found (either because + <span class="domain"><TMPL_VAR NAME=domain></span> has no configured MX, + or because it already uses ours). + </div> + <input type="submit" name="cancel" value="Cancel" /> + </TMPL_IF> + </form> + </div> + </body> +</html> diff --git a/templates/list-domains.html b/templates/list-domains.html index 5ebdd05..5edb4fc 100644 --- a/templates/list-domains.html +++ b/templates/list-domains.html @@ -33,9 +33,18 @@ <tbody> <TMPL_LOOP NAME=domains> <TMPL_IF NAME=__even__><tr class="odd"><TMPL_ELSE><tr></TMPL_IF> - <td><span class="domain"><a href="<TMPL_VAR NAME=domainURI>/"><TMPL_VAR NAME=domain></a></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=isactive><span class="active">✔</span><TMPL_ELSE><span class="inactive">✘</span></TMPL_IF></td> + <td><span class="domain"><TMPL_UNLESS NAME=ispending><a href="<TMPL_VAR NAME=domainURI>/"></TMPL_UNLESS><TMPL_VAR NAME=domain><TMPL_UNLESS NAME=ispending></a></TMPL_UNLESS></span></td> + <td><TMPL_IF NAME=ispending + ><form class="unlock" method="post" action="?" + ><input type="text" name="unlock" size="32" + ><input type="hidden" name="domain" value="<TMPL_VAR NAME=domain>" + /><input type="submit" name="submit" value="Unlock domain" + /></form + ><TMPL_ELSE><TMPL_IF NAME=description><TMPL_VAR NAME=description><TMPL_ELSE><span class="none">(none)</span></TMPL_IF + ></TMPL_IF></td> + <td><TMPL_IF NAME=ispending><span class="pending">⚑</span> + <TMPL_ELSE><TMPL_IF NAME=isactive><span class="active">✔</span> + <TMPL_ELSE><span class="inactive">✘</span></TMPL_IF></TMPL_IF></td> </tr> </TMPL_LOOP> </tbody> diff --git a/templates/list-locals.html b/templates/list-locals.html index 4f21ee8..0c3a3ab 100644 --- a/templates/list-locals.html +++ b/templates/list-locals.html @@ -96,7 +96,7 @@ </TMPL_LOOP> <TMPL_IF NAME=catchalls> <TMPL_IF NAME=CAodd><tr class="odd"><TMPL_ELSE><tr></TMPL_IF> - <td><a href="./?a=edit#catch-all">*</a></td> + <td><a class="dunno" href="./?a=edit#catch-all">—</a></td> <td>Catch-all alias(es) for domain <span class="domain"><TMPL_VAR NAME=domain></span>.</td> <td><span class="dunno">—</span></td> <td><TMPL_LOOP NAME=catchalls><span class="email"><TMPL_VAR NAME=catchall></span><TMPL_UNLESS NAME=__last__>, </TMPL_UNLESS></TMPL_LOOP></td> diff --git a/templates/new-domain.tt b/templates/new-domain.tt new file mode 100644 index 0000000..d42c921 --- /dev/null +++ b/templates/new-domain.tt @@ -0,0 +1,32 @@ +Hi! + + +Someone, hopefully you, has asked Fripost to manage the e-mails under a +domain you own: [% domain %]. If you have not done this, please ignore +this message. + +To add your domain [% domain %] in our system, you need to click on the +following link, or paste it into your favorite browser: + + [% unlockurl %] + +Alternatively, you can log in to the administration panel and paste the +following token in the page that lists pending domains: + + [% token %] + +The token will expire after 24 hours. + + + +Note that adding your domain in our system is not enough to let us +manage your e-mails. You also need to change your MX'es to use ours; +under GNU/Linux you can list our current MX'es with the following +command: + + dig +short fripost.org MX + + +Cheers, +-- +The Fripost administration team. |