From fcf5549584b69e62b6c2f0eb919f6799c7904211 Mon Sep 17 00:00:00 2001 From: "Jannis M. Hoffmann" Date: Fri, 10 Mar 2023 13:54:57 +0100 Subject: Proper recursive rendering of mails to html 1. Added raw mode to model 2. Added raw route 3. Moved readmail view parts to RenderMail plugin 4. Renamed displayheaders partial templates --- lib/JWebmail.pm | 16 ++- lib/JWebmail/Controller/Webmail.pm | 51 ++++---- lib/JWebmail/Model/ReadMails/MockJSON.pm | 7 + lib/JWebmail/Model/ReadMails/MockMaildir.pm | 34 ++--- lib/JWebmail/Model/ReadMails/QMailAuthuser.pm | 76 ++++++++--- lib/JWebmail/Model/ReadMails/Role.pm | 1 + lib/JWebmail/Plugin/Helper.pm | 68 +--------- lib/JWebmail/Plugin/RenderMail.pm | 182 ++++++++++++++++++++++++++ 8 files changed, 296 insertions(+), 139 deletions(-) create mode 100644 lib/JWebmail/Plugin/RenderMail.pm (limited to 'lib') diff --git a/lib/JWebmail.pm b/lib/JWebmail.pm index 3abc78d..6963d44 100644 --- a/lib/JWebmail.pm +++ b/lib/JWebmail.pm @@ -44,6 +44,7 @@ sub startup { $self->plugin('ServerSideSessionData'); } $self->plugin('Helper'); + $self->plugin('RenderMail'); my $i18n_route = $self->plugin('I18N2', $self->config('i18n')); $self->secrets( [$self->config('secret')] ) if $self->config('secret'); @@ -85,17 +86,18 @@ sub route { my $r = shift || $self->routes; - $r->get('/' => 'login')->to('Webmail#noaction'); + $r->get( '/' => 'login')->to('Webmail#noaction'); $r->post('/' => 'login')->to('Webmail#login'); - $r->get('/about')->to('Webmail#about'); - $r->get('/logout')->to('Webmail#logout'); + $r->get('/about' )->to('Webmail#about'); + $r->get('/logout' )->to('Webmail#logout'); my $a = $r->under('/')->to('Webmail#auth'); - $a->get('/home/*folder')->to('Webmail#displayheaders', folder => '')->name('displayheaders'); + $a->get('/home/*folder' )->to('Webmail#displayheaders', folder => '')->name('displayheaders'); $a->get('/read/#id' => 'read')->to('Webmail#readmail'); - $a->get('/write')->to('Webmail#writemail'); - $a->post('/write' => 'send')->to('Webmail#sendmail'); - $a->post('/move')->to('Webmail#move'); + $a->get('/raw/#id' => 'raw' )->to('Webmail#raw'); + $a->get('/write' )->to('Webmail#writemail'); + $a->post('/write' => 'send' )->to('Webmail#sendmail'); + $a->post('/move' )->to('Webmail#move'); } diff --git a/lib/JWebmail/Controller/Webmail.pm b/lib/JWebmail/Controller/Webmail.pm index 6d0cc55..fd1c499 100644 --- a/lib/JWebmail/Controller/Webmail.pm +++ b/lib/JWebmail/Controller/Webmail.pm @@ -73,7 +73,7 @@ sub login { my $passwd = $v->required('password')->size(4, 50)->like(qr/^.+$/)->param; # no new-lines my $challenge; if ($uses_cram) { - $challenge = $v->required('challenge')->size(4, 50)->param; # no new-lines + $challenge = $v->required('challenge')->size(4, 50)->param; } if ($v->has_error) { @@ -190,43 +190,42 @@ sub readmail { my $self = shift; my $mid = $self->stash('id'); - my $auth = $self->stash(ST_AUTH); my $mail; my $ok = eval { $mail = $self->users->show($auth, '', $mid); 1 }; if (!$ok) { - my $err = $@; - if ($err =~ m/unkown mail-id|no such message/) { + my $err = "$@"; + if ($err =~ /unkown mail-id|no such message/) { $self->reply->not_found; return; } die; } + $self->stash(msg => $mail); +} + + +sub raw { + my $self = shift; + + my $mid = $self->stash('id'); + my $auth = $self->stash(ST_AUTH); + # select a single body element my $v = $self->validation; - my $type = $v->optional('body')->like(qr(^[\w\-/; ]+$)a)->param; - return if $v->has_error; + my $path = $v->optional('path')->like(qr(^\d(\.\d)*$)a)->param; + return $self->render(text => 'Issue in parameter "path": '.join(' ', $v->error('path')->@*), status => 400, format => 'txt') if $v->has_error; - if ($type) { - if ($mail->{head}{mime}{content_maintype} eq 'multipart') { - my $content = first {$_->{head}{content_subtype} eq $type} $mail->{body}{parts}->@*; - $self->render(text => $content->{body}); - } - elsif ($mail->{head}{mime}{content_subtype} eq $type) { - $self->render(text => $mail->{body}); - } - else { - $self->reply->not_found; - } - return; - } + my $content = $self->users->raw($auth, '', $mid, $path); - $self->respond_to( - html => {msg => $mail}, - json => {json => $mail} - ); + $self->res->headers->content_disposition(qq[attachment; filename="$content->{head}{filename}"]) + if $content->{head}{content_disposition}; + my $ct = $self->to_mime_type($content->{head}); + if ($ct eq 'text/plain') { $ct .= '; charset=UTF-8' } + $self->res->headers->content_type($ct); + $self->render(data => $content->{body}); } @@ -369,9 +368,3 @@ Sends a mail written in writemail. =head2 move Moves mails between mail forlders. - -=head1 DEPENCIES - -Mojolicious and File::Type - -=cut diff --git a/lib/JWebmail/Model/ReadMails/MockJSON.pm b/lib/JWebmail/Model/ReadMails/MockJSON.pm index 6b3b6d2..e429d53 100644 --- a/lib/JWebmail/Model/ReadMails/MockJSON.pm +++ b/lib/JWebmail/Model/ReadMails/MockJSON.pm @@ -109,6 +109,13 @@ sub show { sub folders { ['', qw(cur test devel debug)] } +sub raw { + my $self = shift; + my ($auth, $folder, $mid, $path) = @_; + + ... +} + sub search { ... } sub move { ... } diff --git a/lib/JWebmail/Model/ReadMails/MockMaildir.pm b/lib/JWebmail/Model/ReadMails/MockMaildir.pm index 9b1bb29..de5c745 100644 --- a/lib/JWebmail/Model/ReadMails/MockMaildir.pm +++ b/lib/JWebmail/Model/ReadMails/MockMaildir.pm @@ -14,12 +14,12 @@ use constant { has user => sub { $ENV{USER} }; has maildir => 't/'; -has extractor => 'perl'; +has extractor => 'python'; our %EXTRACTORS = ( - perl => 'perl script/qmauth.pl', - python => 'python script/qmauth.py', - rust => 'extract/target/debug/jwebmail-extract', + perl => 'script/qmauth.pl', + python => 'script/qmauth.py', + rust => 'bin/jwebmail-extract', ); @@ -51,33 +51,17 @@ sub verify_user { } } -sub build_and_run { +sub start_qmauth { my $self = shift; my ($auth, $mode, $args) = @_; my $mail_user = 'maildir'; - my $exec = $EXTRACTORS{$self->extractor} . ' ' . join(' ', map { my $x = s/(['\\])/\\$1/gr; "'$x'" } ($self->maildir, $self->user, $mail_user, $mode, @$args)); + my @exec = ($EXTRACTORS{$self->extractor}, $self->maildir, $self->user, $mail_user, $mode, @$args); - my $pid = open(my $reader, '-|', $exec) - or die 'failed to create subprocess'; + my $pid = open(my $reader, '-|', @exec) + or die "failed to create subprocess: $!"; - my $input = <$reader>; - - waitpid($pid, 0); - my $rc = $? >> 8; - - my $resp; - if ($rc == 3 || $rc == 0) { - eval { $resp = decode_json $input; }; - if (my $err = $@) { $resp = {error => "decoding error '$err'"}; $rc ||= 1; }; - } - elsif ($rc) { - $resp = {error => "qmauth returned code: $rc"}; - } - - local $" = ', '; - die "error @{[%$resp]}" if $rc; - return $resp; + return $pid, $reader; } diff --git a/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm b/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm index a61cf01..e16e2f2 100644 --- a/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm +++ b/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm @@ -36,7 +36,7 @@ package JWebmail::Model::ReadMails::QMailAuthuser::Error { sub to_string { my $self = shift; - my $verbose = shift; + my $verbose = 1; #shift; if ($verbose && defined $self->{data}) { my $errstr = Data::Dumper->new([$self->{data}])->Terse(1)->Indent(0)->Quotekeys(0)->Dump; @@ -139,6 +139,13 @@ sub show { return $self->build_and_run($auth, 'read', [$folder, $mid]); } +sub raw { + my $self = shift; + my ($auth, $folder, $mid, $path) = @_; + + return $self->build_and_run($auth, 'raw', [$folder, $mid, $path//'']); +} + sub search { my $self = shift; my ($auth, $pattern, $folder) = @_; @@ -175,34 +182,69 @@ sub build_arg { return $self->{qmail_dir}.'/bin/qmail-authuser' . $self->{prefix} . ' ' - . join(' ', map { my $x = s/(['\\])/\\$1/gr; "'$x'" } ($self->{prog}, $self->{maildir}, $self->{user}, $user_name, $mode, @{$args || []})) + . join(' ', map { my $x = s/(['\\])/\\$1/gr; "'$x'" } ($self->{prog}, $self->{maildir}, $self->{user}, $user_name, $mode, @$args)) . ' 3<&0' . ' 2>>'.$self->{logfile}; } -sub execute { - my $_self = shift; - my ($auth, $exec) = @_; +sub start_qmauth { + my $self = shift; + my ($auth, $mode, $args) = @_; + + my $exec = $self->build_arg($auth->{user}, $mode, $args); my $pid = open2(my $reader, my $writer, $exec) - or die 'failed to create subprocess'; + or die "failed to create subprocess: $!"; my $challenge = $auth->{challenge} || ''; $writer->print("$auth->{user}\0$auth->{password}\0$challenge\0") - or die 'pipe wite failed'; + or die "pipe wite failed: $!"; close $writer - or die 'closing write pipe failed'; + or die "closing write pipe failed: $!"; + + return $pid, $reader; +} + +sub read_qmauth { + my $_self = shift; + my ($pid, $reader) = @_; - #binmode $reader, ':encoding(UTF-8)'; my $input = <$reader>; - close $reader - or die 'closing read pipe failed'; - waitpid $pid, 0; - my $rc = $? >> 8; + my $rc; + if (eof $reader) { + # for regular open + close $reader + or warn "closing read pipe failed: $!"; + $rc = $? >> 8; + + # for IPC::Open2 + if (waitpid($pid, 0) == $pid) { + $rc = $? >> 8; + } + } my $resp; - if ($rc == 3 || $rc == 0) { + if (!defined $rc) { + my ($r, $e); + eval { $r = decode_json $input; 1 } + or do { + $rc = 6; + $e = "$@"; + }; + $reader->read(my $buf, 4 * 1024**2); + if (!eof $reader) { + die 'mailpart too large (>4MB)' + } + close $reader; + $resp = { + head => $r, + body => $buf, + rc => $rc, + e => $e, + }; + } + elsif ($rc == 3 || $rc == 0) { eval { $resp = decode_json $input if $input; 1 } or do { $resp = { @@ -214,7 +256,7 @@ sub execute { $rc = 3; }; } - elsif ($rc) { + else { $resp = { info => "got unsuccessful return code by qmail-authuser", return_code => $rc, @@ -229,8 +271,8 @@ sub build_and_run { my $self = shift; my ($auth, $mode, $args) = @_; - my $exec = $self->build_arg($auth->{user}, $mode, $args); - my ($resp, $rc) = $self->execute($auth, $exec); + my @exec = $self->start_qmauth($auth, $mode, $args||[]); + my ($resp, $rc) = $self->read_qmauth(@exec); if ($rc) { JWebmail::Model::ReadMails::QMailAuthuser::Error->throw( diff --git a/lib/JWebmail/Model/ReadMails/Role.pm b/lib/JWebmail/Model/ReadMails/Role.pm index d6fa1e5..ae113de 100644 --- a/lib/JWebmail/Model/ReadMails/Role.pm +++ b/lib/JWebmail/Model/ReadMails/Role.pm @@ -46,6 +46,7 @@ my @methods = ( 'read_headers_for', # auth:Auth, *folder='', *start=0, *end=24, *sort='date' -> ^ :hashref 'search', # auth:Auth, pattern, folder -> ^ :hashref 'show', # auth:Auth, mid -> ^ :hashref + 'raw', ); requires(@methods); diff --git a/lib/JWebmail/Plugin/Helper.pm b/lib/JWebmail/Plugin/Helper.pm index a98f245..b298a17 100644 --- a/lib/JWebmail/Plugin/Helper.pm +++ b/lib/JWebmail/Plugin/Helper.pm @@ -98,45 +98,6 @@ sub parse_iso_date { }; } -sub to_mime_type { - my $c = shift; - my ($mime_head) = @_; - - return "$mime_head->{content_maintype}/$mime_head->{content_subtype}"; -} - - -### mime type html render functions - -my $render_text_plain = sub { - my ($c, $content) = @_; - - $content = xml_escape($content); - $content =~ s/\n/
/g; - - return $content; -}; - -my $render_text_html = sub { - my $c_ = shift; - - return ''; -}; - -our %MIME_Render_Subs = ( - 'text/plain' => $render_text_plain, - 'text/html' => $render_text_html, -); - -sub mime_render { - my ($c, $enc, $cont) = @_; - - ($enc) = $enc =~ m<^(\w+/\w+);?>; - - my $renderer = $MIME_Render_Subs{$enc} // return; - return $renderer->($c, $cont); -}; - ### session password handling @@ -210,7 +171,7 @@ sub warn_crypt { state $once = 0; - if ( !TRUE_RANDOM && !$once && lc($c->config->{session}{secure}) eq 's3d' ) { + if ( !TRUE_RANDOM && !$once && lc $c->config->{session}{secure} eq 's3d' ) { $c->log->warn("Falling back to pseudo random generation. Please install Crypt::URandom"); $once = 1; } @@ -308,8 +269,6 @@ sub register { if contains 'parse_iso_date'; $app->helper(print_sizes2 => sub { shift; print_sizes2(@_) }) if contains 'print_sizes2'; - $app->helper(to_mime_type => \&to_mime_type) - if contains 'to_mime_type'; $app->helper(mime_render => \&mime_render) if contains 'mime_render'; $app->helper(session_passwd => \&session_passwd) @@ -324,7 +283,6 @@ sub register { elsif (!$conf->{import}) { # default imports $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) }); $app->helper(parse_iso_date => sub { shift; parse_iso_date(@_) }); - $app->helper(to_mime_type => \&to_mime_type); $app->helper(mime_render => \&mime_render); $app->helper(session_passwd => \&session_passwd); $app->helper(paginate => \&paginate); @@ -333,6 +291,9 @@ sub register { $app->validator->add_filter(non_empty_ul => \&filter_empty_upload); } + else { + die 'unkown value for "import"' + } } @@ -350,16 +311,10 @@ Helper - Functions used as helpers in controller and templates and additional va use Mojo::Base 'Mojolicious'; - use JWebmail::Plugin::Helper; - sub startup($self) { - $self->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render); + $app->plugin('Helper'); } - # or - - $app->plugin('Helper'); - =head1 DESCRIPTION L provides useful helper functions and validator cheks and filter for @@ -425,15 +380,6 @@ Sets the stash values (all 1 based inclusive): next_page last_page -=head2 mime_render - -A helper for templates used to display the content of a mail for the browser. -The output is valid html and should not be escaped. - - $app->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render); - - %== mime_render 'text/plain' $content - =head2 session_passwd A helper used to set and get the session password. The behavior can be altered by @@ -469,11 +415,11 @@ On log-in it is transfered plainly. =head1 DEPENDENCIES -Mojolicious and optionally Digest::HMAC_MD5, Crypt::URandom. +Mojolicious and recommended Crypt::URandom. =head1 SEE ALSO -L, L, L, L +L =head1 NOTICE diff --git a/lib/JWebmail/Plugin/RenderMail.pm b/lib/JWebmail/Plugin/RenderMail.pm new file mode 100644 index 0000000..4417fae --- /dev/null +++ b/lib/JWebmail/Plugin/RenderMail.pm @@ -0,0 +1,182 @@ +package JWebmail::Plugin::RenderMail; + +use Mojo::Base 'Mojolicious::Plugin'; + +use Mojo::ByteStream; +use Mojo::Util 'xml_escape'; + + +sub render_text_plain { + my ($_c, $_subtype, $content, $_path) = @_; + + $content = xml_escape($content); + $content =~ s/\n/
/g; + + return qq'
\n $content
\n'; +} + +sub render_text_html { + my ($c, $_subtype, $_content, $path) = @_; + + my $url = $c->url_for('raw', id => $c->stash('id')); + $url = $url->query(path => join('.', @$path)) if @$path; + + return qq'\n'; +} + +sub render_multipart_alternative { + my ($c, $_subtype, $content, $path) = @_; + + my $parts = $content->{parts}; + my $R = qq'
'; + my $i = 0; + my $end; + + for (reverse @$parts) { + if (!$end) { + my $x = mime_render($c, to_mime_types($_->{head}), $_->{body}, [@$path, $#$parts-$i]); + if ($x) { + $R .= $x; + $end = 1; + } + } + else { + $R .= '
'; + $R .= ''; + $R .= to_mime_type($_->{head}); + $R .= "\n"; + $R .= mime_render($c, to_mime_types($_->{head}), $_->{body}, [@$path, $#$parts-$i]); + $R .= "
\n"; + } + ++$i; + } + return $R . "
\n"; +} + +sub render_multipart { + my ($c, $_subtype, $content, $path) = @_; + + my $parts = $content->{parts}; + my $R = qq'
'; + my $i = 0; + + for (@$parts) { + if ( !$_->{head}{content_disposition} + || lc $_->{head}{content_disposition} eq 'none' + || lc $_->{head}{content_disposition} eq 'inline') { + + $R .= mime_render($c, to_mime_types($_->{head}), $_->{body}, [@$path, $i]); + } + elsif (lc $_->{head}{content_disposition} eq 'attachment') { + $R .= '

'; + $R .= $c->link_to($c->url_for(raw => id => $c->stash('id'))->query(path => join('.', @$path, $i))->to_abs, (download => $_->{head}{filename}) => sub { + 'Attachment ' . xml_escape($_->{head}{filename}) . ' of type ' . to_mime_type($c, $_->{head}); + }); + $R .= "

\n"; + } + else { + warn "unknown Content-Disposition '$_->{head}{content_disposition}'"; + $R .= "

unknown Content-Disposition '$_->{head}{content_disposition}'

\n"; + } + ++$i; + } + return $R . "
\n"; +} + +sub _format_header { + my ($c, $category, $value) = @_; + + my $R = ''; + + if (ref $value eq 'ARRAY' && $value->@*) { + $R .= '
' . xml_escape(uc $c->l($category)) . "
\n"; + for ($value->@*) { + $R .= '
'; + $R .= xml_escape($_->{display_name} ? qq("$_->{display_name}" <$_->{address}>) : "$_->{address}"); + $R .= "
\n"; + } + } + return $R; +} + +sub render_message { + my ($c, $subtype, $msg, $path) = @_; + + warn "unkown mime-subtype $subtype" unless $subtype eq 'rfc822'; + + my $R .= '
'; + + $R .= '
'; + $R .= '
' . xml_escape(uc $c->l('subject')) . '
'; + $R .= '
' . xml_escape($msg->{head}{subject}) . "
\n"; + $R .= _format_header($c, from => $msg->{head}{from}); + $R .= _format_header($c, to => $msg->{head}{to}); + $R .= _format_header($c, cc => $msg->{head}{cc}); + $R .= _format_header($c, bcc => $msg->{head}{bcc}); + $R .= '
' . xml_escape(uc $c->l('date')) . '
'; + $R .= '
' . xml_escape($msg->{head}{date}) . "
\n"; + $R .= '
' . xml_escape(uc $c->l('content-type')) . '
'; + $R .= '
' . to_mime_type($msg->{head}{mime}) . "
\n"; + $R .= "
\n"; + + #my $content = ref $msg->{body} && exists $msg->{body}{parts} ? $msg->{body}{parts} : $msg->{body}; + + $R .= mime_render($c, to_mime_types($msg->{head}{mime}), $msg->{body}, [@$path, 0]); + + return $R . "
\n"; +} + +our %MIME_Render_Subs = ( + 'text/plain' => \&render_text_plain, + 'text/html' => \&render_text_html, + 'multipart/alternative' => \&render_multipart_alternative, + 'multipart' => \&render_multipart, + 'message' => \&render_message, +); + +sub mime_render { + my ($c, $maintype, $subtype, $content, $path) = @_; + + my $renderer = $MIME_Render_Subs{"$maintype/$subtype"} || $MIME_Render_Subs{$maintype}; + + unless ($renderer) { + return "

Unsupported MIME type of $maintype/$subtype.

\n"; + } + + return $renderer->($c, $subtype, $content, $path); +} + + +sub to_mime_type { lc xml_escape("$_[0]->{content_maintype}/$_[0]->{content_subtype}") } +sub to_mime_types { return xml_escape($_[0]->{content_maintype}), xml_escape($_[0]->{content_subtype}) } + + +sub register { + my ($self, $app, $conf) = @_; + $conf //= {}; + + $app->helper('render_mail.format_mail' => sub { Mojo::ByteStream->new(mime_render($_[0], 'message', 'rfc822', $_[1], [])) }); + $app->helper(to_mime_type => sub { shift; to_mime_type(@_) }); +} + +1 + +__END__ + +=encoding utf-8 + +=head1 NAME + +JWebmail::Plugin::RenderMail - Does the heavy lifting of converting an E-Mail to HTML + +=head1 HELPERS + +=head2 render_mail.format_mail + +Renders a mail to html recursively. + +=head2 to_mime_type + +Combines the content_maintype and content_subtype attributes into the regular MIME description. +These attributes are found in a mail head mime section or as head for multipart messages. + -- cgit v1.2.3