From ee43823179ee627ac16ea9da8168e5f1bf9619c0 Mon Sep 17 00:00:00 2001 From: "Jannis M. Hoffmann" Date: Thu, 29 Oct 2020 12:13:04 +0100 Subject: Initial commit; Stable version --- lib/JWebmail/Controller/Webmail.pm | 386 +++++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 lib/JWebmail/Controller/Webmail.pm (limited to 'lib/JWebmail/Controller/Webmail.pm') diff --git a/lib/JWebmail/Controller/Webmail.pm b/lib/JWebmail/Controller/Webmail.pm new file mode 100644 index 0000000..3ec93f1 --- /dev/null +++ b/lib/JWebmail/Controller/Webmail.pm @@ -0,0 +1,386 @@ +package JWebmail::Controller::Webmail; + +use Mojo::Base 'Mojolicious::Controller'; + +use File::Type; + +use constant { + S_USER => 'user', # Key for user name in active session +}; + + +# no action has been taken, display login page +sub noaction { + my $self = shift; + + my $user = $self->session(S_USER); + if ($user) { + $self->res->code(307); + $self->redirect_to('home'); + } +} + + +# middleware +sub auth { + my $self = shift; + + my $user = $self->session(S_USER); + my $pw = $self->session_passwd; + + unless ($user && $pw) { + $self->flash(message => $self->l('no_session')); + $self->res->code(401); + $self->redirect_to('logout'); + return 0; + } + + return 1; +} + + +sub _time :prototype(&$$) { + my $code = shift; + my $self = shift; + my $name = shift; + + $self->timing->begin($name); + + my @res = $code->(); + + my $elapsed = $self->timing->elapsed($name); + $self->app->log->debug("$name took $elapsed seconds"); + + return wantarray ? @res : $res[-1]; +} + + +sub login { + my $self = shift; + + my $v = $self->validation; + + my $user = $v->required('userid')->size(4, 50)->param; + my $passwd = $v->required('password')->size(4, 50)->like(qr/^.+$/)->param; # no new-lines + + if ($v->has_error) { + $self->res->code(400); + return $self->render(action => 'noaction'); + } + + my $valid = _time { $self->users->verify_user($user, $passwd) } $self, 'verify user'; + + if ($valid) { + $self->session(S_USER() => $user); + $self->session_passwd($passwd); + + $self->res->code(303); + $self->redirect_to('displayheaders'); + } + else { + $self->res->code(401); + $self->render(action => 'noaction', + warning => $self->l('login') . ' ' . $self->l('failed') . '!', + ); + } +} + + +sub logout { + my $self = shift; + + delete $self->session->{S_USER()}; + $self->session_passwd(''); + + # $self->session(expires => 1); + + $self->res->code(303); + $self->redirect_to('noaction'); +} + + +sub about { + my $self = shift; + + $self->stash( + scriptadmin => $self->config->{defaults}{scriptadmin}, + http_host => $self->tx->req->url->to_abs->host, + request_uri => $self->tx->req->url, + remote_addr => $self->tx->original_remote_address, + ); +} + + +sub displayheaders { + no warnings 'experimental::smartmatch'; + my $self = shift; + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $folders = _time { $self->users->folders($auth) } $self, 'user folders'; + push @$folders, ''; + + unless ( $self->stash('folder') ~~ $folders ) { + $self->res->code(404); + $self->render(template => 'error', + error => $self->l('no_folder'), + links => [map { $self->url_for(folder => $_) } @$folders], + ); + return; + } + + my $v = $self->validation; + my $sort = $v->optional('sort')->like(qr'^!?(?:date|subject|sender|size)$')->param // '!date'; + my $search = $v->optional('search')->param; + + if ($v->has_error) { + $self->res->code(400); + $self->render(template => 'error', error => "errors in @{ $v->failed }"); + return; + } + + my ($total_byte_size, $cnt, $new) = _time { $self->users->count($auth, $self->stash('folder')) } $self, 'user count'; + + my ($start, $end) = $self->paginate($cnt); + + $self->timing->begin('user_headers'); + my $headers; + if ($search) { + $headers = $self->users->search( + $auth, $search, $self->stash('folder'), + ); + } + else { + $headers = $self->users->read_headers_for( + auth => $auth, + folder => $self->stash('folder'), + start => $start, + end => $end, + sort => $sort, + ); + } + my $elapsed = $self->timing->elapsed('user_headers'); + $self->app->log->debug("Reading user headers took $elapsed seconds"); + + $self->stash( + msgs => $headers, + mail_folders => $folders, + total_size => $total_byte_size, + total_new_mails => $new, + ); +} + + +sub readmail { + my $self = shift; + + my $mid = $self->stash('id'); + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $mail; + eval { $mail = $self->users->show($auth, $mid) }; + if (my $err = $@) { + if ($err =~ m/unkown mail-id|no such message/) { + $self->reply->not_found; + return; + } + die $@; + } + + $self->render(action => 'readmail', + msg => $mail, + ); +} + + +sub writemail { } + + +sub sendmail { + my $self = shift; + + my %mail; + my $v = $self->validation; + $v->csrf_protect; + + $mail{to} = $v->required('to', 'not_empty')->check('mail_line')->every_param; + $mail{message} = $v->required('body', 'not_empty')->param; + $mail{subject} = $v->required('subject', 'not_empty')->param; + $mail{cc} = $v->optional('cc', 'not_empty')->check('mail_line')->every_param; + $mail{bcc} = $v->optional('bcc', 'not_empty')->check('mail_line')->every_param; + $mail{reply} = $v->optional('back_to', 'not_empty')->check('mail_line')->param; + $mail{attach} = $v->optional('attach', 'non_empty_ul')->upload->param; + $mail{attach_type} = File::Type->new()->mime_type($mail{attach}->asset->get_chunk(0, 512)) if $mail{attach}; + $mail{from} = $self->session(S_USER); + + if ($v->has_error) { + $self->log->debug("mail send failed. Error in @{ $v->failed }"); + + $self->render(action => 'writemail', + warning => $self->l('error_send'), + ); + return; + } + + my $error = $self->send_mail(\%mail); + + if ($error) { + $v->error('send'=> ['internal_error']); # make validation fail so that values are restored + + $self->render(action => 'writemail', + warning => $self->l('error_send'), + ); + return; + } + + $self->flash(message => $self->l('succ_send')); + $self->res->code(303); + $self->redirect_to('displayheaders'); +} + + +sub move { + my $self = shift; + + my $v = $self->validation; + $v->csrf_protect; + + if ($v->has_error) { + return; + } + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + my $folders = $self->users->folders($auth); + + my $mm = $self->every_param('mail'); + my $folder = $self->param('folder'); + + no warnings 'experimental::smartmatch'; + die "$folder not valid" unless $folder ~~ $folders; + + $self->users->move($auth, $_, $folder) for @$mm; + + $self->flash(message => $self->l('succ_move')); + $self->res->code(303); + $self->redirect_to('displayheaders'); +} + + +sub raw { + my $self = shift; + + my $mid = $self->stash('id'); + + my $auth = AuthReadMails->new( + user => $self->session(S_USER), + password => $self->session_passwd, + challenge => $self->app->secrets->[0], + ); + + my $mail = $self->users->show($auth, $mid); + + if ($self->param('body')//'' eq 'html') { + if ($mail->{content_type} eq 'text/html') { + $self->render(text => $mail->{body}) ; + } + elsif ($mail->{content_type} eq 'multipart/alternative') { + my ($content) = grep {$_->{type} eq 'text/html'} @{ $mail->{body} }; + $self->render(text => $content->{val}); + } + else { + $self->res->code(404); + } + } + else { + $self->res->headers->content_type('text/plain'); + $self->render(text => $self->dumper($mail)); + } +} + + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +Webmail - All functions comprising the webmail application. + +=head1 SYNOPSIS + + my $r = $app->routes; + $r->get('/about')->to('Webmail#about'); + $r->post('/login')->to('Webmail#login'); + +=head1 DESCRIPTION + +The controller of JWebmail. + +=head1 METHODS + +=head2 noaction + +The login page. This should be the root. + +=head2 auth + + my $a = $r->under('/')->to('Webmail#auth'); + + An intermediate route that makes sure a user has a valid session. + +=head2 login + +Post route that checks login data. + +=head2 logout + +Route that clears session data. + +=head2 about + +Public route. + +=head2 displayheaders + +Provides an overview over messages. + +=head2 readmail + +Displays a single mail. + +=head2 writemail + +A mail editor. + +=head2 sendmail + +Sends a mail written in writemail. + +=head2 move + +Moves mails between mail forlders. + +=head2 raw + +Displays the mail raw, ready to be downloaded. + +=head1 DEPENCIES + +Mojolicious and File::Type + +=cut \ No newline at end of file -- cgit v1.2.3