First commit working UI V1
This commit is contained in:
commit
324a7c6a5e
49 changed files with 23199 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
dbic.conf
|
||||
dump.conf
|
||||
my-mailserver.production.conf
|
||||
my-mailserver.*.conf
|
||||
mail/*
|
||||
60
bin/alias.pl
Normal file
60
bin/alias.pl
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#! /usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use 5.020;
|
||||
use Getopt::Long::Descriptive;
|
||||
use lib 'lib';
|
||||
use My::Mailserver::Schema;
|
||||
|
||||
my ( $opt, $usage ) = describe_options(
|
||||
"domain.pl %o",
|
||||
[ 'source|s=s', 'The source of the alias' ],
|
||||
[ 'target|t=s', 'The destination for the alias' ],
|
||||
[ 'domain|d=s', 'The domain to operate on' ],
|
||||
[ 'add|a', 'Add a new domain (requires domain)' ],
|
||||
[ 'remove|r', 'Remove a domain (requires domain)' ],
|
||||
[ 'list|l', 'List all domains' ],
|
||||
[ 'help', 'Print usage message and exit', { shortcircuit => 1 } ],
|
||||
);
|
||||
|
||||
say( $usage->text ), exit if $opt->help;
|
||||
|
||||
my $schema = My::Mailserver::Schema->connect('MAILSERVER_MANAGER');
|
||||
|
||||
my $rs = $schema->resultset('VirtualAlias');
|
||||
|
||||
if ( $opt->list ) {
|
||||
my $line = "%5s | %20s | %20s | %20s\n";
|
||||
printf $line, 'ID', 'Domain', 'Alias', 'Destination';
|
||||
while ( my $result = $rs->next ) {
|
||||
printf $line, $result->id, $result->domain->name, $result->source, $result->destination;
|
||||
}
|
||||
}
|
||||
elsif ( $opt->domain && $opt->source && $opt->target ) {
|
||||
my $domain = $schema->resultset('VirtualDomain')->find({ name => $opt->domain });
|
||||
my $alias = $domain->find_or_new_related('virtual_aliases', {
|
||||
source => $opt->source,
|
||||
destination => $opt->target
|
||||
});
|
||||
if ( $opt->add && $domain ) {
|
||||
if ( $alias->in_storage ) {
|
||||
die "Alias already exists";
|
||||
} else {
|
||||
$alias->insert;
|
||||
say "Created new alias";
|
||||
}
|
||||
}
|
||||
elsif ( $opt->remove ) {
|
||||
if ( $alias->in_storage ) {
|
||||
$alias->delete;
|
||||
say "Deleted alias";
|
||||
} else {
|
||||
die "Alias not found";
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
die "Must provide domain, source, and target";
|
||||
}
|
||||
55
bin/domain.pl
Normal file
55
bin/domain.pl
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#! /usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use 5.020;
|
||||
use Getopt::Long::Descriptive;
|
||||
use lib 'lib';
|
||||
use My::Mailserver::Schema;
|
||||
|
||||
my ( $opt, $usage ) = describe_options(
|
||||
"domain.pl %o",
|
||||
[ 'domain|d=s', 'The domain to operate on' ],
|
||||
[ 'add|a', 'Add a new domain (requires domain)' ],
|
||||
[ 'remove|r', 'Remove a domain (requires domain)' ],
|
||||
[ 'list|l', 'List all domains' ],
|
||||
[ 'help', 'Print usage message and exit', { shortcircuit => 1 } ],
|
||||
);
|
||||
|
||||
say( $usage->text ), exit if $opt->help;
|
||||
|
||||
my $schema = My::Mailserver::Schema->connect('MAILSERVER_MANAGER');
|
||||
|
||||
my $rs = $schema->resultset('VirtualDomain');
|
||||
|
||||
if ( $opt->list ) {
|
||||
my $line = "%5s | %70s\n";
|
||||
printf $line, 'ID', 'Domain';
|
||||
while ( my $result = $rs->next ) {
|
||||
printf $line, $result->id, $result->name;
|
||||
}
|
||||
}
|
||||
elsif ( $opt->add && $opt->domain ) {
|
||||
my $domain = $rs->find_or_new({ name => $opt->domain });
|
||||
|
||||
if ( $domain->in_storage ) {
|
||||
die "Domain already exists";
|
||||
} else {
|
||||
$domain->insert;
|
||||
say "Created new domain";
|
||||
}
|
||||
}
|
||||
elsif ($opt->remove && $opt->domain ) {
|
||||
my $domain = $rs->find({name => $opt->domain });
|
||||
|
||||
if ( $domain ) {
|
||||
$domain->delete;
|
||||
say "Deleted domain";
|
||||
} else {
|
||||
die "Domain not found";
|
||||
}
|
||||
}
|
||||
else {
|
||||
die "Must provide domain";
|
||||
}
|
||||
72
bin/user.pl
Normal file
72
bin/user.pl
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#! /usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use 5.020;
|
||||
use Getopt::Long::Descriptive;
|
||||
use lib 'lib';
|
||||
use My::Mailserver::Schema;
|
||||
|
||||
my ( $opt, $usage ) = describe_options(
|
||||
"domain.pl %o",
|
||||
[ 'email|e=s', 'The users new email' ],
|
||||
[ 'password|p=s', 'The password for the user' ],
|
||||
[ 'domain|d=s', 'The domain to operate on' ],
|
||||
[ 'add|a', 'Add a new user (requires domain, email and password)' ],
|
||||
[ 'remove|r', 'Remove a user (requires domain and email)' ],
|
||||
[ 'update|u', 'Update user (requires domain, email and password' ],
|
||||
[ 'list|l', 'List all users' ],
|
||||
[ 'help', 'Print usage message and exit', { shortcircuit => 1 } ],
|
||||
);
|
||||
|
||||
say( $usage->text ), exit if $opt->help;
|
||||
|
||||
my $schema = My::Mailserver::Schema->connect('MAILSERVER_MANAGER');
|
||||
|
||||
my $rs = $schema->resultset('VirtualUser');
|
||||
|
||||
if ( $opt->list ) {
|
||||
my $line = "%5s | %20s | %20s\n";
|
||||
printf $line, 'ID', 'Domain', 'Email';
|
||||
while ( my $result = $rs->next ) {
|
||||
printf $line, $result->id, $result->domain->name, $result->email;
|
||||
}
|
||||
}
|
||||
elsif ( $opt->domain && $opt->email ) {
|
||||
my $domain = $schema->resultset('VirtualDomain')->find({ name => $opt->domain });
|
||||
my $alias = $domain->find_or_new_related('virtual_users', {
|
||||
email => $opt->email,
|
||||
});
|
||||
if ( $opt->add && $domain ) {
|
||||
if ( $alias->in_storage ) {
|
||||
die "Alias already exists";
|
||||
} elsif( $opt->password ) {
|
||||
$alias->password( $opt->password );
|
||||
$alias->insert;
|
||||
say "Created new user";
|
||||
} else {
|
||||
die "Requires password";
|
||||
}
|
||||
}
|
||||
elsif ( $opt->update && $domain ) {
|
||||
if ( $alias->in_storage ) {
|
||||
$alias->password( $opt->password );
|
||||
$alias->update;
|
||||
say "Updated password";
|
||||
} else {
|
||||
die "User not found";
|
||||
}
|
||||
}
|
||||
elsif ( $opt->remove ) {
|
||||
if ( $alias->in_storage ) {
|
||||
$alias->delete;
|
||||
say "Deleted user";
|
||||
} else {
|
||||
die "User not found";
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
die "Must provide domain and email";
|
||||
}
|
||||
13
dbic.conf.example
Normal file
13
dbic.conf.example
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<MAILSERVER_MANAGER>
|
||||
dsn dbi:mysql:mailserver
|
||||
user some_username
|
||||
password some_password
|
||||
</MAILSERVER_MANAGER>
|
||||
<MAILSERVER_RUNTIME>
|
||||
dsn dbi:mysql:mailserver
|
||||
user some_username
|
||||
password some_password
|
||||
</MAILSERVER_RUNTIME>
|
||||
<MAILSERVER_TESTING>
|
||||
dsn dbi:sqlite::memory:
|
||||
</MAILSERVER_TESTING>
|
||||
90
lib/My/Mailserver.pm
Normal file
90
lib/My/Mailserver.pm
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package My::Mailserver;
|
||||
use Mojo::Base 'Mojolicious';
|
||||
use My::Mailserver::Schema;
|
||||
use Mojo::Loader 'load_class';
|
||||
|
||||
has db => sub {
|
||||
my $c = shift;
|
||||
return My::Mailserver::Schema->connect($c->app->config->{db_config});
|
||||
};
|
||||
|
||||
has email_transport => sub {
|
||||
my $c = shift;
|
||||
my $transport = $c->app->config->{mail_transport};
|
||||
my $transport_class = "Email::Sender::Transport::$transport";
|
||||
my $e = load_class $transport_class;
|
||||
die qq{Loading "$transport_class" failed: $e} if ref $e;
|
||||
return $transport_class->new($c->app->config->{mail_transport_config} // {});
|
||||
};
|
||||
|
||||
# This method will run once at server start
|
||||
sub startup {
|
||||
my $self = shift;
|
||||
|
||||
push @{ $self->commands->namespaces }, 'My::Mailserver::Command';
|
||||
|
||||
# Load configuration from hash returned by config file
|
||||
my $config = $self->plugin('Config', {
|
||||
default => {
|
||||
mail_transport => 'Maildir',
|
||||
mail_transport_config => {
|
||||
dir => $self->home->child('mail'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
# Configure the application
|
||||
$self->secrets($config->{secrets});
|
||||
|
||||
$self->plugin('Authentication' => {
|
||||
'load_user' => sub {
|
||||
my ( $c, $user_id ) = @_;
|
||||
return $c->schema->resultset('VirtualUser')->find($user_id);
|
||||
},
|
||||
'validate_user' => sub {
|
||||
my ( $c, $email, $password, $args) = @_;
|
||||
my $user = $c->schema->resultset('VirtualUser')->find({email => $email});
|
||||
if ( defined $user ) {
|
||||
if ( $user->check_password( $password ) ) {
|
||||
return $user->id;
|
||||
}
|
||||
}
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
$self->helper( schema => sub { $self->db } );
|
||||
|
||||
$self->helper( email_transport => sub { $self->email_transport });
|
||||
|
||||
# Router
|
||||
my $r = $self->routes;
|
||||
|
||||
$r->get('/')->to('auth#login_get');
|
||||
$r->post('/')->to('auth#login_post');
|
||||
$r->post('/recover')->to('auth#recover_post');
|
||||
$r->get('/reset/:code')->to('auth#reset_get');
|
||||
$r->post('/reset/:code')->to('auth#reset_post');
|
||||
$r->any('/logout')->to('auth#logout_any');
|
||||
|
||||
my $auth_route = $r->under('/dashboard')->to('auth#under');
|
||||
|
||||
$auth_route->get('/')->to('dashboard#index');
|
||||
$auth_route->post('/change_password')->to('dashboard#change_password');
|
||||
$auth_route->post('/change_recovery')->to('dashboard#change_recovery');
|
||||
|
||||
my $manager_route = $auth_route->under('/manager')->to('auth#is_manager');
|
||||
|
||||
$manager_route->get('/')->to('manager#index');
|
||||
$manager_route->get('/domains')->to('manager#domains');
|
||||
$manager_route->get('/users')->to('manager#users');
|
||||
$manager_route->get('/alias')->to('manager#alias');
|
||||
$manager_route->post('/add_domain')->to('manager#add_domain');
|
||||
$manager_route->post('/del_domain')->to('manager#del_domain');
|
||||
$manager_route->post('/add_user')->to('manager#add_user');
|
||||
$manager_route->post('/del_user')->to('manager#del_user');
|
||||
$manager_route->post('/add_alias')->to('manager#add_alias');
|
||||
$manager_route->post('/del_alias')->to('manager#del_alias');
|
||||
}
|
||||
|
||||
1;
|
||||
52
lib/My/Mailserver/Command/roundcube_password.pm
Normal file
52
lib/My/Mailserver/Command/roundcube_password.pm
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package My::Mailserver::Command::roundcube_password;
|
||||
use Mojo::Base 'Mojolicious::Command';
|
||||
|
||||
use Mojo::Util qw/ getopt dumper /;
|
||||
|
||||
has description => sub { 'Manage passwords using roundcube password plugin' };
|
||||
has usage => sub { shift->extract_usage };
|
||||
|
||||
sub run {
|
||||
my ( $self, @args ) = @_;
|
||||
|
||||
my $command = '';
|
||||
|
||||
print dumper \@args;
|
||||
getopt \@args,
|
||||
'command=s' => \$command,
|
||||
'user=s' => \my $user,
|
||||
'old_pass=s' => \my $old_pass,
|
||||
'new_pass=s' => \my $new_pass;
|
||||
|
||||
print dumper $command;
|
||||
|
||||
if ( $command eq 'save' ) { $self->save( $old_pass, $new_pass, $user ) }
|
||||
else { die "Command not recognised" }
|
||||
}
|
||||
|
||||
sub save {
|
||||
my ( $self, $old_pass, $new_pass, $email ) = @_;
|
||||
|
||||
my $user = $self->app->schema->resultset('VirtualUser')->find({ email => $email });
|
||||
|
||||
if ( $user ) {
|
||||
print dumper $user->email;
|
||||
if ( $user->check_password( $old_pass ) ) {
|
||||
$user->password( $new_pass );
|
||||
$user->update;
|
||||
print "PASSWORD_SUCCESS";
|
||||
} else {
|
||||
print "PASSWORD_ERROR";
|
||||
}
|
||||
} else {
|
||||
print "PASSWORD_CONNECT_ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Usage: APPLICATION roundcube_password [OPTIONS]
|
||||
|
||||
=cut
|
||||
90
lib/My/Mailserver/Controller/Auth.pm
Normal file
90
lib/My/Mailserver/Controller/Auth.pm
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package My::Mailserver::Controller::Auth;
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
use Email::Stuffer;
|
||||
|
||||
sub under {
|
||||
my $c = shift;
|
||||
if ($c->is_user_authenticated) {
|
||||
$c->current_user->recovery_codes->delete;
|
||||
return 1;
|
||||
}
|
||||
$c->redirect_to('/');
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub is_manager {
|
||||
...
|
||||
}
|
||||
|
||||
sub login_get {
|
||||
my $c = shift;
|
||||
$c->redirect_to('/dashboard') if $c->is_user_authenticated;
|
||||
}
|
||||
|
||||
sub login_post {
|
||||
my $c = shift;
|
||||
|
||||
my $v = $c->validation;
|
||||
|
||||
$v->csrf_protect;
|
||||
$v->required('email');
|
||||
$v->required('password');
|
||||
|
||||
if ($v->is_valid && $c->authenticate($v->param('email'), $v->param('password'))) {
|
||||
return $c->redirect_to('/dashboard');
|
||||
};
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
|
||||
sub recover_post {
|
||||
my $c = shift;
|
||||
my $v = $c->validation;
|
||||
|
||||
$v->csrf_protect;
|
||||
$v->required('recovery_email');
|
||||
|
||||
if ($v->is_valid) {
|
||||
my $email = $c->schema->resultset('VirtualUser')->find({email => $v->param('recovery_email')});
|
||||
if (defined $email) {
|
||||
my $recovery = $email->new_recovery_code;
|
||||
$c->app->log->debug('Created recovery token: ' . $recovery->code);
|
||||
my $url = $c->url_for('/reset/' . $recovery->code)->to_abs;
|
||||
my $email_body = $c->render_to_string(
|
||||
template => '_email/reset',
|
||||
email => $email,
|
||||
url => $url,
|
||||
format => 'txt',
|
||||
);
|
||||
Email::Stuffer
|
||||
->from('postmaster@' . $email->domain->name)
|
||||
->to($email->email)
|
||||
->subject('Password Reset')
|
||||
->text_body($email_body)
|
||||
->transport($c->email_transport)
|
||||
->send_or_die;
|
||||
} else {
|
||||
$c->app->log->debug('Email not found');
|
||||
}
|
||||
} else {
|
||||
$c->app->log->debug('Invalid Form');
|
||||
}
|
||||
# You'l never know.... :D
|
||||
$c->flash(success => 'If that email exists, a recovery link has been sent');
|
||||
return $c->redirect_to('/');
|
||||
}
|
||||
|
||||
sub reset_get {
|
||||
my $self = shift;
|
||||
}
|
||||
|
||||
sub reset_post {
|
||||
my $self = shift;
|
||||
}
|
||||
|
||||
sub logout_any {
|
||||
my $c = shift;
|
||||
$c->logout;
|
||||
$c->redirect_to('/');
|
||||
}
|
||||
|
||||
1;
|
||||
60
lib/My/Mailserver/Controller/Dashboard.pm
Normal file
60
lib/My/Mailserver/Controller/Dashboard.pm
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package My::Mailserver::Controller::Dashboard;
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
|
||||
sub index {
|
||||
my $c = shift;
|
||||
|
||||
$c->stash->{domain} = $c->current_user->domain;
|
||||
$c->stash->{aliases} = $c->schema->resultset('VirtualAlias')->search(
|
||||
{ destination => $c->current_user->email },
|
||||
{ order_by => 'source' }
|
||||
);
|
||||
$c->stash->{recovery_email} = $c->current_user->recovery_email;
|
||||
}
|
||||
|
||||
sub change_password {
|
||||
my $c = shift;
|
||||
my $v = $c->validation;
|
||||
|
||||
$v->csrf_protect;
|
||||
$v->required('current_password');
|
||||
$v->required('new_password');
|
||||
$v->required('confirm_password');
|
||||
|
||||
if ($v->is_valid) {
|
||||
if ($c->current_user->check_password($v->param('current_password'))) {
|
||||
if ($v->param('new_password') eq $v->param('confirm_password')) {
|
||||
$c->current_user->password($v->param('new_password'));
|
||||
$c->current_user->update;
|
||||
$c->flash(success => 'Password changed successfully');
|
||||
} else {
|
||||
$c->flash(error => 'New and Confirm password must match');
|
||||
}
|
||||
} else {
|
||||
$c->flash(error => 'Current password incorrect');
|
||||
}
|
||||
} else {
|
||||
$c->app->log->debug('invalid form');
|
||||
$c->flash(error => 'All form fields required');
|
||||
}
|
||||
$c->redirect_to('/dashboard');
|
||||
}
|
||||
|
||||
sub change_recovery {
|
||||
my $c = shift;
|
||||
my $v = $c->validation;
|
||||
|
||||
$v->csrf_protect;
|
||||
$v->required('recovery_email');
|
||||
|
||||
if ($v->is_valid) {
|
||||
$c->current_user->recovery_email($v->param('recovery_email'));
|
||||
$c->current_user->update;
|
||||
$c->flash(success => 'Recovery email changed successfully');
|
||||
} else {
|
||||
$c->flash(error => 'Failed to change recovery email');
|
||||
}
|
||||
$c->redirect_to('/dashboard');
|
||||
}
|
||||
|
||||
1;
|
||||
8
lib/My/Mailserver/Controller/Manager.pm
Normal file
8
lib/My/Mailserver/Controller/Manager.pm
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package My::Mailserver::Controller::Manager;
|
||||
use Mojo::Base 'Mojolicious::Controller';
|
||||
|
||||
sub index {
|
||||
my $self = shift;
|
||||
}
|
||||
|
||||
1;
|
||||
20
lib/My/Mailserver/Schema.pm
Normal file
20
lib/My/Mailserver/Schema.pm
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Schema::Config';
|
||||
|
||||
__PACKAGE__->load_namespaces;
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-06-23 14:49:31
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:2K/yVBuVVVwGBBwJAlOoyw
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
99
lib/My/Mailserver/Schema/Result/ManagersDomain.pm
Normal file
99
lib/My/Mailserver/Schema/Result/ManagersDomain.pm
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema::Result::ManagersDomain;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
My::Mailserver::Schema::Result::ManagersDomain
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<managers_domains>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("managers_domains");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 domain_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 manager_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"domain_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"manager_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</domain_id>
|
||||
|
||||
=item * L</manager_id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("domain_id", "manager_id");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 domain
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualDomain>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"domain",
|
||||
"My::Mailserver::Schema::Result::VirtualDomain",
|
||||
{ id => "domain_id" },
|
||||
{ is_deferrable => 1, on_delete => "RESTRICT", on_update => "RESTRICT" },
|
||||
);
|
||||
|
||||
=head2 manager
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualUser>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"manager",
|
||||
"My::Mailserver::Schema::Result::VirtualUser",
|
||||
{ id => "manager_id" },
|
||||
{ is_deferrable => 1, on_delete => "RESTRICT", on_update => "RESTRICT" },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-06-17 02:39:38
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:N9hivlqaxQB72BI7BH9tZA
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
118
lib/My/Mailserver/Schema/Result/RecoveryCode.pm
Normal file
118
lib/My/Mailserver/Schema/Result/RecoveryCode.pm
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema::Result::RecoveryCode;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
My::Mailserver::Schema::Result::RecoveryCode
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<recovery_codes>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("recovery_codes");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 user_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 code
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 created
|
||||
|
||||
data_type: 'timestamp'
|
||||
datetime_undef_if_invalid: 1
|
||||
default_value: 'current_timestamp()'
|
||||
is_nullable: 0
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"user_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"code",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
"created",
|
||||
{
|
||||
data_type => "timestamp",
|
||||
datetime_undef_if_invalid => 1,
|
||||
default_value => "current_timestamp()",
|
||||
is_nullable => 0,
|
||||
},
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("id");
|
||||
|
||||
=head1 UNIQUE CONSTRAINTS
|
||||
|
||||
=head2 C<code>
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</code>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_unique_constraint("code", ["code"]);
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 user
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualUser>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"user",
|
||||
"My::Mailserver::Schema::Result::VirtualUser",
|
||||
{ id => "user_id" },
|
||||
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-06-17 02:39:38
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:UeAp7iDyLd6gYYoanmZEfg
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
98
lib/My/Mailserver/Schema/Result/VirtualAlias.pm
Normal file
98
lib/My/Mailserver/Schema/Result/VirtualAlias.pm
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema::Result::VirtualAlias;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
My::Mailserver::Schema::Result::VirtualAlias
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<virtual_aliases>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("virtual_aliases");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 domain_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 source
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 destination
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"domain_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"source",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
"destination",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("id");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 domain
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualDomain>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"domain",
|
||||
"My::Mailserver::Schema::Result::VirtualDomain",
|
||||
{ id => "domain_id" },
|
||||
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
|
||||
);
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-06-15 22:53:38
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:otU7xR9XhoziLALEVhQUZw
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
146
lib/My/Mailserver/Schema/Result/VirtualDomain.pm
Normal file
146
lib/My/Mailserver/Schema/Result/VirtualDomain.pm
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema::Result::VirtualDomain;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
My::Mailserver::Schema::Result::VirtualDomain
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<virtual_domains>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("virtual_domains");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 name
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 imap_host
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=head2 smtp_host
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=head2 mail_host
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"name",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
"imap_host",
|
||||
{ data_type => "varchar", is_nullable => 1, size => 255 },
|
||||
"smtp_host",
|
||||
{ data_type => "varchar", is_nullable => 1, size => 255 },
|
||||
"mail_host",
|
||||
{ data_type => "varchar", is_nullable => 1, size => 255 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("id");
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 managers_domains
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::ManagersDomain>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"managers_domains",
|
||||
"My::Mailserver::Schema::Result::ManagersDomain",
|
||||
{ "foreign.domain_id" => "self.id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 virtual_aliases
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualAlias>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"virtual_aliases",
|
||||
"My::Mailserver::Schema::Result::VirtualAlias",
|
||||
{ "foreign.domain_id" => "self.id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 virtual_users
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualUser>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"virtual_users",
|
||||
"My::Mailserver::Schema::Result::VirtualUser",
|
||||
{ "foreign.domain_id" => "self.id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 managers
|
||||
|
||||
Type: many_to_many
|
||||
|
||||
Composing rels: L</managers_domains> -> manager
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->many_to_many("managers", "managers_domains", "manager");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-07-07 04:05:03
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zFGn1x/l57uM9QohW5mwjg
|
||||
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
208
lib/My/Mailserver/Schema/Result/VirtualUser.pm
Normal file
208
lib/My/Mailserver/Schema/Result/VirtualUser.pm
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
use utf8;
|
||||
package My::Mailserver::Schema::Result::VirtualUser;
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader
|
||||
# DO NOT MODIFY THE FIRST PART OF THIS FILE
|
||||
|
||||
=head1 NAME
|
||||
|
||||
My::Mailserver::Schema::Result::VirtualUser
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
=head1 TABLE: C<virtual_users>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->table("virtual_users");
|
||||
|
||||
=head1 ACCESSORS
|
||||
|
||||
=head2 id
|
||||
|
||||
data_type: 'integer'
|
||||
is_auto_increment: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 domain_id
|
||||
|
||||
data_type: 'integer'
|
||||
is_foreign_key: 1
|
||||
is_nullable: 0
|
||||
|
||||
=head2 password
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 email
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 0
|
||||
size: 255
|
||||
|
||||
=head2 recovery_email
|
||||
|
||||
data_type: 'varchar'
|
||||
is_nullable: 1
|
||||
size: 255
|
||||
|
||||
=head2 is_manager
|
||||
|
||||
data_type: 'tinyint'
|
||||
default_value: 0
|
||||
is_nullable: 1
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
"id",
|
||||
{ data_type => "integer", is_auto_increment => 1, is_nullable => 0 },
|
||||
"domain_id",
|
||||
{ data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
|
||||
"password",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
"email",
|
||||
{ data_type => "varchar", is_nullable => 0, size => 255 },
|
||||
"recovery_email",
|
||||
{ data_type => "varchar", is_nullable => 1, size => 255 },
|
||||
"is_manager",
|
||||
{ data_type => "tinyint", default_value => 0, is_nullable => 1 },
|
||||
);
|
||||
|
||||
=head1 PRIMARY KEY
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</id>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->set_primary_key("id");
|
||||
|
||||
=head1 UNIQUE CONSTRAINTS
|
||||
|
||||
=head2 C<email>
|
||||
|
||||
=over 4
|
||||
|
||||
=item * L</email>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->add_unique_constraint("email", ["email"]);
|
||||
|
||||
=head1 RELATIONS
|
||||
|
||||
=head2 domain
|
||||
|
||||
Type: belongs_to
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::VirtualDomain>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
"domain",
|
||||
"My::Mailserver::Schema::Result::VirtualDomain",
|
||||
{ id => "domain_id" },
|
||||
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" },
|
||||
);
|
||||
|
||||
=head2 managers_domains
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::ManagersDomain>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"managers_domains",
|
||||
"My::Mailserver::Schema::Result::ManagersDomain",
|
||||
{ "foreign.manager_id" => "self.id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 recovery_codes
|
||||
|
||||
Type: has_many
|
||||
|
||||
Related object: L<My::Mailserver::Schema::Result::RecoveryCode>
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many(
|
||||
"recovery_codes",
|
||||
"My::Mailserver::Schema::Result::RecoveryCode",
|
||||
{ "foreign.user_id" => "self.id" },
|
||||
{ cascade_copy => 0, cascade_delete => 0 },
|
||||
);
|
||||
|
||||
=head2 domains
|
||||
|
||||
Type: many_to_many
|
||||
|
||||
Composing rels: L</managers_domains> -> domain
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->many_to_many("domains", "managers_domains", "domain");
|
||||
|
||||
|
||||
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2019-06-17 02:39:38
|
||||
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:U24kyZS/j+9Gvs2gGlVBIw
|
||||
|
||||
__PACKAGE__->load_components('PassphraseColumn');
|
||||
__PACKAGE__->add_columns(
|
||||
'+password' => {
|
||||
passphrase => 'crypt',
|
||||
passphrase_class => 'BlowfishCrypt',
|
||||
passphrase_args => {
|
||||
salt_random => 1,
|
||||
cost => 8,
|
||||
},
|
||||
passphrase_check_method => 'check_password',
|
||||
},
|
||||
);
|
||||
|
||||
sub new_recovery_code {
|
||||
my $self = shift;
|
||||
|
||||
my $code;
|
||||
my $max_tries = 10;
|
||||
while (1) {
|
||||
$code = eval {
|
||||
$self->create_related('recovery_codes', {
|
||||
code => _gen_code(),
|
||||
});
|
||||
};
|
||||
warn $max_tries;
|
||||
last if $code;
|
||||
last unless $max_tries;
|
||||
$max_tries--;
|
||||
}
|
||||
die "Error generating code" unless $code;
|
||||
return $code;
|
||||
}
|
||||
|
||||
use String::Random;
|
||||
|
||||
sub _gen_code {
|
||||
my $gen = String::Random->new;
|
||||
$gen->{'A'} = [@{$gen->{'C'}}, @{$gen->{'c'}}, @{$gen->{'n'}}];
|
||||
return $gen->randpattern('A' x 64);
|
||||
}
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
1;
|
||||
4
my-mailserver.conf
Normal file
4
my-mailserver.conf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
secrets => ['thisisonlyatest'],
|
||||
db_config => 'MAILSERVER_TESTING',
|
||||
}
|
||||
1912
public/css/bootstrap-grid.css
vendored
Normal file
1912
public/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
public/css/bootstrap-grid.css.map
Normal file
1
public/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
public/css/bootstrap-grid.min.css
vendored
Normal file
7
public/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/bootstrap-grid.min.css.map
Normal file
1
public/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
331
public/css/bootstrap-reboot.css
vendored
Normal file
331
public/css/bootstrap-reboot.css
vendored
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.1.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-ms-overflow-style: scrollbar;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
@-ms-viewport {
|
||||
width: device-width;
|
||||
}
|
||||
|
||||
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
[tabindex="-1"]:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-original-title] {
|
||||
text-decoration: underline;
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: .5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:not([href]):not([tabindex]):focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
-ms-overflow-style: scrollbar;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
caption-side: bottom;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: 1px dotted;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
button,
|
||||
html [type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
input[type="month"] {
|
||||
-webkit-appearance: listbox;
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: .5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
public/css/bootstrap-reboot.css.map
Normal file
1
public/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
public/css/bootstrap-reboot.min.css
vendored
Normal file
8
public/css/bootstrap-reboot.min.css
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*!
|
||||
* Bootstrap Reboot v4.1.3 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2018 The Bootstrap Authors
|
||||
* Copyright 2011-2018 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
||||
1
public/css/bootstrap-reboot.min.css.map
Normal file
1
public/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
9030
public/css/bootstrap.css
vendored
Normal file
9030
public/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
public/css/bootstrap.css.map
Normal file
1
public/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
public/css/bootstrap.min.css
vendored
Normal file
7
public/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/bootstrap.min.css.map
Normal file
1
public/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
6461
public/js/bootstrap.bundle.js
vendored
Normal file
6461
public/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
public/js/bootstrap.bundle.js.map
Normal file
1
public/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/bootstrap.bundle.min.js.map
Normal file
1
public/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
3944
public/js/bootstrap.js
vendored
Normal file
3944
public/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
public/js/bootstrap.js.map
Normal file
1
public/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
public/js/bootstrap.min.js
vendored
Normal file
7
public/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/bootstrap.min.js.map
Normal file
1
public/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
11
script/my_mailserver
Normal file
11
script/my_mailserver
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use FindBin;
|
||||
BEGIN { unshift @INC, "$FindBin::Bin/../lib" }
|
||||
use Mojolicious::Commands;
|
||||
|
||||
# Start command line interface for application
|
||||
Mojolicious::Commands->start_app('My::Mailserver');
|
||||
24
sql/001-mail_setup.sql
Normal file
24
sql/001-mail_setup.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE `virtual_domains` (
|
||||
`id` int(11) NOT NULL auto_increment,
|
||||
`name` varchar(50) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `virtual_users` (
|
||||
`id` int(11) NOT NULL auto_increment,
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`email` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `email` (`email`),
|
||||
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `virtual_aliases` (
|
||||
`id` int(11) NOT NULL auto_increment,
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`source` varchar(255) NOT NULL,
|
||||
`destination` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
20
sql/002-user_setup.sql
Normal file
20
sql/002-user_setup.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
CREATE TABLE `managers_domains` (
|
||||
`domain_id` int(11) NOT NULL,
|
||||
`manager_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`domain_id`, `manager_id`),
|
||||
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id),
|
||||
FOREIGN KEY (manager_id) REFERENCES virtual_users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE `recovery_codes` (
|
||||
`id` int(11) NOT NULL auto_increment,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`code` varchar(255) NOT NULL,
|
||||
`created` timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `code` (`code`),
|
||||
FOREIGN KEY (user_id) REFERENCES virtual_users(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
ALTER TABLE `virtual_users` ADD COLUMN `recovery_email` varchar(255) DEFAULT NULL;
|
||||
ALTER TABLE `virtual_users` ADD COLUMN `is_manager` BOOLEAN DEFAULT FALSE;
|
||||
5
sql/003-domain_info.sql
Normal file
5
sql/003-domain_info.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
ALTER TABLE `virtual_domains` CHANGE COLUMN `name` `name` varchar(255) NOT NULL;
|
||||
ALTER TABLE `virtual_domains` ADD COLUMN `imap_host` varchar(255) DEFAULT NULL;
|
||||
ALTER TABLE `virtual_domains` ADD COLUMN `smtp_host` varchar(255) DEFAULT NULL;
|
||||
ALTER TABLE `virtual_domains` ADD COLUMN `mail_host` varchar(255) DEFAULT NULL;
|
||||
|
||||
11
sql/dump.conf.example
Normal file
11
sql/dump.conf.example
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
schema_class My::Mailserver::Schema
|
||||
|
||||
<connect_info>
|
||||
dsn dbi:mysql:mailserver
|
||||
user username
|
||||
pass some password
|
||||
</connect_info>
|
||||
|
||||
<loader_options>
|
||||
dump_directory ./lib
|
||||
</loader_options>
|
||||
14
t/auth.t
Normal file
14
t/auth.t
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use Mojo::Base -strict;
|
||||
|
||||
use Test::More;
|
||||
use Test::Mojo;
|
||||
|
||||
my $t = Test::Mojo->new('My::Mailserver');
|
||||
$t->get_ok('/')
|
||||
->status_is(200)
|
||||
->content_like(qr/Login/i)
|
||||
->element_exists('input[name=email][type=text]')
|
||||
->element_exists('input[name=password][type=password]')
|
||||
->element_exists('input[name=csrf_token][type=hidden]');
|
||||
|
||||
done_testing();
|
||||
9
t/basic.t
Normal file
9
t/basic.t
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use Mojo::Base -strict;
|
||||
|
||||
use Test::More;
|
||||
use Test::Mojo;
|
||||
|
||||
my $t = Test::Mojo->new('My::Mailserver');
|
||||
$t->get_ok('/')->status_is(200)->content_like(qr/Login/i);
|
||||
|
||||
done_testing();
|
||||
13
templates/_email/reset.txt.ep
Normal file
13
templates/_email/reset.txt.ep
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Hi,
|
||||
|
||||
A password reset was requested for <%= $email->email %>. Please visit
|
||||
the following url to reset:
|
||||
|
||||
<%= $url %>
|
||||
|
||||
The url will be valid for 1 hour. If you did not request this reset,
|
||||
then either ignore this message or log into your account normally.
|
||||
|
||||
Thanks,
|
||||
|
||||
Postmaster
|
||||
44
templates/auth/login_get.html.ep
Normal file
44
templates/auth/login_get.html.ep
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
% layout 'default';
|
||||
% title 'Login';
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto">
|
||||
<h1>Login</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto">
|
||||
<form action="<%= url_for '/' %>" method="POST" class="form">
|
||||
<div class="form-group">
|
||||
<%= label_for 'email' => 'Email' %>
|
||||
<%= text_field 'email' => (class => 'form-control', id => 'email') %>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<%= label_for 'password' => 'Password' %>
|
||||
<%= password_field 'password' => (class => 'form-control', id => 'password') %>
|
||||
</div>
|
||||
<%= csrf_field %>
|
||||
<button type="submit" class="btn btn-primary btn-block">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto mt-3">
|
||||
<button class="btn btn-link" data-toggle="collapse" data-target="#recovery-form" href="#">Lost Password?</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto collapse" id="recovery-form">
|
||||
<form action="<%= url_for '/recover' %>" method="post">
|
||||
<div class="form-group">
|
||||
<%= label_for 'recovery_email' => 'Email' %>
|
||||
<%= text_field 'recovery_email' => (class => 'form-control', id => 'recovery_email') %>
|
||||
</div>
|
||||
<%= csrf_field %>
|
||||
<button type="submit" class="btn btn-primary btn-block">Send Recovery Email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
89
templates/dashboard/index.html.ep
Normal file
89
templates/dashboard/index.html.ep
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
% layout 'main';
|
||||
% title 'Dashboard';
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto">
|
||||
<h1>Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
<% if ( my $error = flash('error') ) { %>
|
||||
<div class="row">
|
||||
<div class="col alert alert-warning">
|
||||
<span class="text-black-50">Error:</span> <%= $error %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if ( my $success = flash('success') ) { %>
|
||||
<div class="row">
|
||||
<div class="col alert alert-success">
|
||||
<span class="text-black-50">Error:</span> <%= $success %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 card-columns">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Info</h5>
|
||||
<p class="card-text">Information for accessing either the hosted mail option, or setting up your mail software.</p>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Webmail: <%= link_to $domain->mail_host => $domain->mail_host %></li>
|
||||
<li class="list-group-item">IMAP: <%= $domain->imap_host %></li>
|
||||
<li class="list-group-item">SMTP: <%= $domain->smtp_host %></li>
|
||||
<li class="list-group-item">Use your email and password for authentication on both IMAP and SMTP</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Aliases</h5>
|
||||
<p class="card-text">All Aliases set up for you</p>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<% foreach my $alias ( $aliases->all ) { %>
|
||||
<li class="list-group-item"><%= $alias->source %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Change Password</h5>
|
||||
<p class="card-text">Gotta have some way of doing it, after all...</p>
|
||||
<form action="<%= url_for '/dashboard/change_password' %>" method="post">
|
||||
<div class="form-group">
|
||||
<label for="current_password">Current Password</label>
|
||||
<input type="password" class="form-control" id="current_password" name="current_password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_password">New Password</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password">
|
||||
</div>
|
||||
<%= csrf_field %>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Recovery Email</h5>
|
||||
<p class="card-text">In the chance you forget your password, this at least lets you recover it!</p>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Current Address: <%= $recovery_email %></li>
|
||||
</ul>
|
||||
<form action="<%= url_for '/dashboard/change_recovery' %>" method="post">
|
||||
<div class="form-group">
|
||||
<label for="current_password">Email</label>
|
||||
<input type="email" class="form-control" id="recovery_email" name="recovery_email">
|
||||
</div>
|
||||
<%= csrf_field %>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
11
templates/layouts/default.html.ep
Normal file
11
templates/layouts/default.html.ep
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body><%= content %></body>
|
||||
</html>
|
||||
26
templates/layouts/main.html.ep
Normal file
26
templates/layouts/main.html.ep
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="<%= url_for '/dashboard' %>">Mail Dashboard</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<%= url_for '/logout' %>">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<%= content %>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue