diff options
author | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-03-10 13:54:57 +0100 |
---|---|---|
committer | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-03-10 13:54:57 +0100 |
commit | fcf5549584b69e62b6c2f0eb919f6799c7904211 (patch) | |
tree | e5f0e480af0f39f1c0f457ea0aca8d33f8fb4d0b /lib/JWebmail | |
parent | df59f9dec32d7f8f08706fd3eb5b784deaa0abfc (diff) |
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
Diffstat (limited to 'lib/JWebmail')
-rw-r--r-- | lib/JWebmail/Controller/Webmail.pm | 51 | ||||
-rw-r--r-- | lib/JWebmail/Model/ReadMails/MockJSON.pm | 7 | ||||
-rw-r--r-- | lib/JWebmail/Model/ReadMails/MockMaildir.pm | 34 | ||||
-rw-r--r-- | lib/JWebmail/Model/ReadMails/QMailAuthuser.pm | 76 | ||||
-rw-r--r-- | lib/JWebmail/Model/ReadMails/Role.pm | 1 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/Helper.pm | 68 | ||||
-rw-r--r-- | lib/JWebmail/Plugin/RenderMail.pm | 182 |
7 files changed, 287 insertions, 132 deletions
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/<br>/g; - - return $content; -}; - -my $render_text_html = sub { - my $c_ = shift; - - return '<iframe src="' . $c_->url_for('read', id => $c_->stash('id'))->query(body => 'html') . '" class=html-mail></iframe>'; -}; - -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<JWebmail::Helper> 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<JWebmail>, L<JWebmail::Controller::All>, L<Mojolicious>, L<Mojolicious::Controller> +L<JWebmail> =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/<br>/g; + + return qq'<div class="jwm-mail-body jwm-mail-body-text-plain">\n $content </div>\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'<iframe src="$url" class="jwm-mail-body-text-html" ></iframe>\n'; +} + +sub render_multipart_alternative { + my ($c, $_subtype, $content, $path) = @_; + + my $parts = $content->{parts}; + my $R = qq'<div class="jwm-mail-body jwm-mail-body-multipart-alternative"\n>'; + 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 .= '<details class="jwm-mail-body-multipart-alternative-extra" >'; + $R .= '<summary>'; + $R .= to_mime_type($_->{head}); + $R .= "</summary>\n"; + $R .= mime_render($c, to_mime_types($_->{head}), $_->{body}, [@$path, $#$parts-$i]); + $R .= "</details>\n"; + } + ++$i; + } + return $R . "</div>\n"; +} + +sub render_multipart { + my ($c, $_subtype, $content, $path) = @_; + + my $parts = $content->{parts}; + my $R = qq'<div class="jwm-mail-body jwm-mail-body-multipart"\n>'; + 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 .= '<p>'; + $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 .= "</p>\n"; + } + else { + warn "unknown Content-Disposition '$_->{head}{content_disposition}'"; + $R .= "<p>unknown Content-Disposition '$_->{head}{content_disposition}'</p>\n"; + } + ++$i; + } + return $R . "</div>\n"; +} + +sub _format_header { + my ($c, $category, $value) = @_; + + my $R = ''; + + if (ref $value eq 'ARRAY' && $value->@*) { + $R .= '<dt>' . xml_escape(uc $c->l($category)) . "</dt>\n"; + for ($value->@*) { + $R .= '<dd>'; + $R .= xml_escape($_->{display_name} ? qq("$_->{display_name}" <$_->{address}>) : "$_->{address}"); + $R .= "<dd>\n"; + } + } + return $R; +} + +sub render_message { + my ($c, $subtype, $msg, $path) = @_; + + warn "unkown mime-subtype $subtype" unless $subtype eq 'rfc822'; + + my $R .= '<div clas="jwm-mail">'; + + $R .= '<dl class="jwm-mail-header">'; + $R .= '<dt>' . xml_escape(uc $c->l('subject')) . '</dt>'; + $R .= '<dd>' . xml_escape($msg->{head}{subject}) . "</dd>\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 .= '<dt>' . xml_escape(uc $c->l('date')) . '</dt>'; + $R .= '<dd>' . xml_escape($msg->{head}{date}) . "</dd>\n"; + $R .= '<dt>' . xml_escape(uc $c->l('content-type')) . '</dt>'; + $R .= '<dd>' . to_mime_type($msg->{head}{mime}) . "</dd>\n"; + $R .= "</dl>\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 . "</div>\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 "<p>Unsupported MIME type of <code>$maintype/$subtype</code>.</p>\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. + |