summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2023-03-10 13:54:57 +0100
committerJannis M. Hoffmann <jannis@fehcom.de>2023-03-10 13:54:57 +0100
commitfcf5549584b69e62b6c2f0eb919f6799c7904211 (patch)
treee5f0e480af0f39f1c0f457ea0aca8d33f8fb4d0b
parentdf59f9dec32d7f8f08706fd3eb5b784deaa0abfc (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
-rw-r--r--CHANGES.md11
-rw-r--r--MANIFEST15
-rw-r--r--lib/JWebmail.pm16
-rw-r--r--lib/JWebmail/Controller/Webmail.pm51
-rw-r--r--lib/JWebmail/Model/ReadMails/MockJSON.pm7
-rw-r--r--lib/JWebmail/Model/ReadMails/MockMaildir.pm34
-rw-r--r--lib/JWebmail/Model/ReadMails/QMailAuthuser.pm76
-rw-r--r--lib/JWebmail/Model/ReadMails/Role.pm1
-rw-r--r--lib/JWebmail/Plugin/Helper.pm68
-rw-r--r--lib/JWebmail/Plugin/RenderMail.pm182
-rw-r--r--public/style.css8
-rwxr-xr-xscript/qmauth.py138
-rw-r--r--templates/displayheaders/_bot_nav.html.ep (renamed from templates/headers/_display_bot_nav.html.ep)2
-rw-r--r--templates/displayheaders/_folders.html.ep (renamed from templates/headers/_display_folders.html.ep)0
-rw-r--r--templates/displayheaders/_main_table.html.ep (renamed from templates/headers/_display_headers.html.ep)2
-rw-r--r--templates/displayheaders/_pagination1.html.ep (renamed from templates/headers/_pagination1.html.ep)0
-rw-r--r--templates/displayheaders/_pagination2.html.ep (renamed from templates/headers/_pagination2.html.ep)0
-rw-r--r--templates/displayheaders/_top_nav.html.ep (renamed from templates/headers/_display_top_nav.html.ep)2
-rw-r--r--templates/exception_.html.ep (renamed from templates/error.html.ep)8
-rw-r--r--templates/webmail/displayheaders.html.ep8
-rw-r--r--templates/webmail/readmail.html.ep89
21 files changed, 440 insertions, 278 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 1e9d2c4..fbcf729 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -134,13 +134,14 @@ Current v1.1.0
- [ ] moving mails to other folders
- [ ] creating new folders
- [ ] backend
-- [ ] specify protocol for backend interaction (RPC IDL)
+- [x] specify protocol for backend interaction (RPC IDL)
- [ ] choose tool for validation
- [ ] cleanup README
- [ ] improve about page
- [ ] improve performance of backend, consider alternatives to Mail::Box::Manager
- [ ] based on Maildir::Light
- [x] reimplementation in Rust
+ - [x] reimplementation in Python
- [ ] cache mail reads
- is unlikely to work as mails can be moved freely
- [ ] add exception types
@@ -153,12 +154,18 @@ Current v1.1.0
- [ ] add config validation
- [ ] improve i18n
- [ ] add localization of dates and time
+ - [ ] plural handling
- [x] refactor I18N plugin to allow independent translate provider
- [x] extend matcher dynamic with a role
-
- [x] compute hmac on the client side
- [x] better handling on form
- [ ] better random numbers
+- [x] separate namespace for pagination in the stash
+
+- [ ] Rust IO error ??
+- [x] Read attachments
+- [x] display recursive multipart
+- [ ] iframe load confirmation
Future
------
diff --git a/MANIFEST b/MANIFEST
index bd96af3..3d6f0c9 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -19,6 +19,7 @@ lib/JWebmail/Plugin/I18N2.pm
lib/JWebmail/Plugin/I18N2/Role.pm
lib/JWebmail/Plugin/I18N2/INI.pm
lib/JWebmail/Plugin/I18N2/Maketext.pm
+lib/JWebmail/Plugin/RenderMail.pm
lib/JWebmail/Plugin/ServerSideSessionData.pm
t/Webmail.t
@@ -32,14 +33,14 @@ templates/webmail/displayheaders.html.ep
templates/webmail/about.html.ep
templates/webmail/login.html.ep
templates/layouts/mainlayout.html.ep
-templates/headers/_display_top_nav.html.ep
-templates/headers/_display_bot_nav.html.ep
-templates/headers/_display_folders.html.ep
-templates/headers/_display_headers.html.ep
-templates/headers/_pagination2.html.ep
-templates/headers/_pagination1.html.ep
+templates/displayheaders/_top_nav.html.ep
+templates/displayheaders/_bot_nav.html.ep
+templates/displayheaders/_folders.html.ep
+templates/displayheaders/_main_table.html.ep
+templates/displayheaders/_pagination2.html.ep
+templates/displayheaders/_pagination1.html.ep
templates/not_found_.html.ep
-templates/error.html.ep
+templates/exception_.html.ep
public/style.css
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/<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.
+
diff --git a/public/style.css b/public/style.css
index 16bc419..a62ef9c 100644
--- a/public/style.css
+++ b/public/style.css
@@ -62,10 +62,10 @@ footer {
padding: 10px;
}
- .jwm-mail-body > iframe.html-mail {
- width: 100%;
- height: 400px;
- }
+iframe.jwm-mail-body-text-html {
+ width: 100%;
+ height: 400px;
+}
.jwm-mail-body-text-plain {
background-color: var(--jwm-color-alt);
diff --git a/script/qmauth.py b/script/qmauth.py
index 38e5d58..662b39c 100755
--- a/script/qmauth.py
+++ b/script/qmauth.py
@@ -31,6 +31,7 @@ from argparse import ArgumentParser
from datetime import datetime
from functools import cache
from glob import glob
+from itertools import islice
from mailbox import Maildir, MaildirMessage
from os import environ, getpid, path, setuid, stat
from pathlib import Path
@@ -51,7 +52,7 @@ class MyMaildir(Maildir):
p_new = glob(path.join(self.__path, 'new', mid + '*'))
res = p_cur + p_new
if len(res) != 1:
- raise LookupError("could not uniquely identify file for mail-id")
+ raise LookupError(f"could not uniquely identify file for mail-id {mid!r}", mid)
return res[0]
def get_folder(self, folder):
@@ -61,6 +62,13 @@ class MyMaildir(Maildir):
)
+class QMAuthError(Exception):
+
+ def __init__(self, msg, **args):
+ self.msg = msg
+ self.info = args
+
+
@cache
def _file_size(fname):
return stat(fname).st_size
@@ -81,24 +89,34 @@ def _get_rcv_time(mid):
return float(mid[:idx])
-def startup(maildir, su, user, headers_only):
+def startup(maildir, su, user, mode):
del environ['PATH']
netfehcom_uid = getpwnam(su).pw_uid
- assert netfehcom_uid, "must be non root"
+ if not netfehcom_uid:
+ logging.error("user must not be root")
+ sysexit(5)
try:
setuid(netfehcom_uid)
except OSError:
logging.exception("error setting uid")
sysexit(5)
+ def create_messages(mail_file):
+ if mode == count_mails:
+ msg = MaildirMessage(None)
+ elif mode == list_mails:
+ msg = MaildirMessage(email.parser.BytesHeaderParser(policy=email.policy.default).parse(mail_file))
+ else:
+ msg = email.parser.BytesParser(policy=email.policy.default).parse(mail_file)
+
+ return msg
+
return MyMaildir(
maildir / user,
create=False,
- factory=lambda x: MaildirMessage(
- email.parser.BytesParser(policy=email.policy.default).parse(x, headersonly=headers_only)
- ),
+ factory=create_messages,
)
@@ -203,32 +221,36 @@ def count_mails(f, subfolder):
def _get_body(mail):
if not mail.is_multipart():
if mail.get_content_maintype() == 'text':
- return mail.get_payload(decode=True).decode(
- encoding=mail.get_content_charset(failobj='utf-8'),
- errors='replace')
+ return mail.get_content()
else:
- return mail.get_payload()
+ ret = mail.get_content()
+ if ret.isascii():
+ return ret.decode(encoding='ascii')
+ raise ValueError(f"unsupported content type in leaf part {mail.get_content_type()}")
if (mctype := mail.get_content_maintype()) == 'message':
- mm = mail.get_payload()
- assert len(mm) == 1
- msg = mm[0]
+ msg = mail.get_content()
return {
'head': _get_head_info(msg),
- 'body': msg.get_content(),
+ 'body': _get_body(msg),
}
elif mctype == 'multipart':
- return {
+ ret = {
'preamble': mail.preamble,
- 'parts': [
- {
- 'head': _get_mime_head_info(part),
- 'body': _get_body(part),
- }
- for part in mail.get_payload()
- ],
+ 'parts': [],
'epilogue': mail.epilogue,
}
+ for part in mail.iter_parts():
+ head = _get_mime_head_info(part)
+ if not head['content_disposition'] == 'attachment':
+ body = _get_body(part)
+ else:
+ body = None
+ ret['parts'].append({
+ 'head': head,
+ 'body': body,
+ })
+ return ret
else:
raise ValueError(f"unknown major content-type {mctype!r}")
@@ -237,9 +259,9 @@ def read_mail(f, subfolder, mid):
if subfolder:
f = f.get_folder(subfolder)
- msg = f[mid]
+ msg = f.get(mid, None)
if not msg:
- return {'error': "no such message", 'mid': mid}
+ raise QMAuthError("no such message", mid=mid)
return {
'head': _get_head_info(msg),
@@ -247,6 +269,60 @@ def read_mail(f, subfolder, mid):
}
+def _descent(xx):
+ head = _get_mime_head_info(xx)
+ if (mctype := head['content_maintype']) == 'message':
+ body = list(xx.iter_parts())[0]
+ elif mctype == 'multipart':
+ body = xx.iter_parts()
+ else:
+ body = xx.get_content()
+ return {
+ 'head': head,
+ 'body': body,
+ }
+
+
+def raw_mail(f, subfolder, mid, path):
+ if subfolder:
+ f = f.get_folder(subfolder)
+
+ pth = [int(seg) for seg in path.split('.')] if path else []
+ mail = {
+ 'head': {"content_maintype": "message", "content_subtype": "rfc822"},
+ 'body': f[mid],
+ }
+
+ for n in pth:
+ mctype = mail['head']['content_maintype']
+
+ if mctype == 'multipart':
+ try:
+ res = next(islice(mail['body'], n, None))
+ except StopIteration:
+ raise QMAuthError("out of bounds path for mail", path=pth)
+ mail = _descent(res)
+ elif mctype == 'message':
+ assert n == 0
+ mail = _descent(mail['body'])
+ else:
+ raise QMAuthError(f"can not descent into non multipart content type {mctype}")
+
+ if hasattr(mail['body'], '__next__'):
+ raise QMAuthError("can not stop at multipart section", path=pth)
+
+ json.dump(mail['head'], stdout)
+ stdout.write("\n")
+ if type(mail['body']) is str:
+ stdout.write(mail['body'])
+ elif type(mail['body']) is bytes:
+ stdout.flush()
+ stdout.buffer.write(mail['body'])
+ else:
+ stdout.write(str(mail['body']))
+ sysexit(0)
+
+
def _matches(m, pattern):
if m.is_multipart():
return any(
@@ -317,6 +393,12 @@ sp_read.add_argument('subfolder')
sp_read.add_argument('mid', metavar='message')
sp_read.set_defaults(run=read_mail)
+sp_raw = sp.add_parser('raw')
+sp_raw.add_argument('subfolder')
+sp_raw.add_argument('mid', metavar='message')
+sp_raw.add_argument('path', default='')
+sp_raw.set_defaults(run=raw_mail)
+
sp_folders = sp.add_parser('folders')
sp_folders.set_defaults(run=folders)
@@ -344,14 +426,16 @@ if __name__ == '__main__':
args.pop('maildir_path'),
args.pop('os_user'),
args.pop('mail_user'),
- args['run'] == list_mails,
+ args['run'],
)
logging.debug("setuid successful")
run = args.pop('run')
reply = run(s, **args)
json.dump(reply, stdout)
- if isinstance(reply, dict) and 'error' in reply:
- sysexit(3)
+ except QMAuthError as qerr:
+ errmsg = dict(error=qerr.msg, **qerr.info)
+ json.dump(errmsg, stdout)
+ sysexit(3)
except Exception:
logging.exception("qmauth.py error")
sysexit(4)
diff --git a/templates/headers/_display_bot_nav.html.ep b/templates/displayheaders/_bot_nav.html.ep
index 9dc808a..3eb57d3 100644
--- a/templates/headers/_display_bot_nav.html.ep
+++ b/templates/displayheaders/_bot_nav.html.ep
@@ -1,7 +1,7 @@
<div class="pure-g jwm-nav">
<div class="pure-u-3-4 pure-u-md-1-4">
- %= include 'headers/_pagination1'
+ %= include 'displayheaders/_pagination1'
</div>
<div class="pure-u-1-4 pure-u-md-1-4">
diff --git a/templates/headers/_display_folders.html.ep b/templates/displayheaders/_folders.html.ep
index be5bdd9..be5bdd9 100644
--- a/templates/headers/_display_folders.html.ep
+++ b/templates/displayheaders/_folders.html.ep
diff --git a/templates/headers/_display_headers.html.ep b/templates/displayheaders/_main_table.html.ep
index 6c00d0b..5430c15 100644
--- a/templates/headers/_display_headers.html.ep
+++ b/templates/displayheaders/_main_table.html.ep
@@ -76,7 +76,7 @@
<!--
<div class="pure-u-1-4">
- %= ucfirst($msg->{head}{mime}{content_maintype} eq 'multipart' ? l('yes') : l('no'));
+ %# ucfirst($msg->{head}{mime}{content_maintype} eq 'multipart' ? l('yes') : l('no'));
</div>
-->
diff --git a/templates/headers/_pagination1.html.ep b/templates/displayheaders/_pagination1.html.ep
index 798f79f..798f79f 100644
--- a/templates/headers/_pagination1.html.ep
+++ b/templates/displayheaders/_pagination1.html.ep
diff --git a/templates/headers/_pagination2.html.ep b/templates/displayheaders/_pagination2.html.ep
index 8bff0bf..8bff0bf 100644
--- a/templates/headers/_pagination2.html.ep
+++ b/templates/displayheaders/_pagination2.html.ep
diff --git a/templates/headers/_display_top_nav.html.ep b/templates/displayheaders/_top_nav.html.ep
index ca5001b..fd6bae6 100644
--- a/templates/headers/_display_top_nav.html.ep
+++ b/templates/displayheaders/_top_nav.html.ep
@@ -23,7 +23,7 @@
</div>
<div class="pure-u-1-1 pure-u-md-1-2">
- %= include 'headers/_pagination2';
+ %= include 'displayheaders/_pagination2';
</div>
<!-- delete button
diff --git a/templates/error.html.ep b/templates/exception_.html.ep
index 3a1d5e3..a605aaf 100644
--- a/templates/error.html.ep
+++ b/templates/exception_.html.ep
@@ -10,7 +10,7 @@
<h1>Error</h1>
<p class=center>
% if (my $msg = stash 'error') {
- %= $msg
+%= $msg
% }
% else {
Uwu :(
@@ -20,10 +20,10 @@
% if (my $see_other = stash 'links') {
See:
<nav>
- % for (@$see_other) {
- %= link_to $_ => $_
+% for (@$see_other) {
+%= link_to $_ => $_
<br>
- % }
+% }
</nav>
% }
</body>
diff --git a/templates/webmail/displayheaders.html.ep b/templates/webmail/displayheaders.html.ep
index 23f48d3..3f650c0 100644
--- a/templates/webmail/displayheaders.html.ep
+++ b/templates/webmail/displayheaders.html.ep
@@ -2,7 +2,7 @@
<div id=displayheaders>
- %= include 'headers/_display_folders';
+ %= include 'displayheaders/_folders';
% if (my $loginmessage = stash 'loginmessage') {
<p id=loginmessage>
@@ -10,10 +10,10 @@
</p>
% }
- %= include 'headers/_display_top_nav';
+ %= include 'displayheaders/_top_nav';
% if (@$msgs) {
- %= include 'headers/_display_headers';
+ %= include 'displayheaders/_main_table';
% }
% else {
<p id=empty-folder>
@@ -21,6 +21,6 @@
</p>
% }
- %= include 'headers/_display_bot_nav';
+ %= include 'displayheaders/_bot_nav';
</div>
diff --git a/templates/webmail/readmail.html.ep b/templates/webmail/readmail.html.ep
index e1f299d..5bdc27e 100644
--- a/templates/webmail/readmail.html.ep
+++ b/templates/webmail/readmail.html.ep
@@ -1,98 +1,11 @@
% layout 'mainlayout';
-% my $mail_fmt = begin
-% my ($category, $value) = @_;
-% if (ref $value eq 'ARRAY' && $value->@*) {
- <dt> <%= uc l $category %> </dt>
-% for ($value->@*) {
- <dd>
-%= $_->{display_name} ? qq("$_->{display_name}" <$_->{address}>) : "$_->{address}"
- </dd>
-% }
-% }
-% end
-
-
-% my $format_mail = begin
-% my ($msg) = @_;
-% my $body = $msg->{body};
-% my $content_type = to_mime_type $msg->{head}{mime};
-
- <div clas="jwm-mail">
-
- <dl class="jwm-mail-header">
- <dt> <%= uc l 'subject' %> </dt>
- <dd> <%= $msg->{head}{subject} %> </dd>
-%= $mail_fmt->(from => $msg->{head}{from})
-%= $mail_fmt->(to => $msg->{head}{to})
-%= $mail_fmt->(cc => $msg->{head}{cc})
-%= $mail_fmt->(bcc => $msg->{head}{bcc})
- <dt> <%= uc l 'date' %> </dt>
- <dd> <%= $msg->{head}{date} %> </dd>
- <dt> <%= uc l 'content-type' %> </dt>
- <dd> <%= $content_type %> </dd>
- </dl>
-
-% if ($content_type eq 'multipart/alternative') {
-% my $end;
-% for (reverse $body->{parts}->@*) {
-% if (!$end) {
- <div class="jwm-mail-body <%= to_mime_type($_->{head}) eq 'text/plain' ? 'jwm-mail-body-text-plain' : '' %>" >
-% my $x = mime_render(to_mime_type($_->{head}), $_->{body});
-%== $x;
- </div>
-% $end = 1 if $x;
-% }
-% else {
- <details class="jwm-mail-body <%= to_mime_type($_->{head}) eq 'text/plain' ? 'jwm-mail-body-text-plain' : '' %>" >
- <summary>
-%= to_mime_type $_->{head}
- </summary>
-%== mime_render(to_mime_type($_->{head}), $_->{body})
- </details>
-% }
-% }
-% }
-% elsif ($msg->{head}{mime}{content_maintype} eq 'multipart') {
-% for ($body->{parts}->@*) {
-% if ( !$_->{head}{content_disposition}
-% || lc $_->{head}{content_disposition} eq 'none'
-% || lc $_->{head}{content_disposition} eq 'inline') {
- <div class="jwm-mail-body <%= to_mime_type($_->{head}) eq 'text/plain' ? 'jwm-mail-body-text-plain' : '' %>" >
-%== mime_render(to_mime_type($_->{head}), $_->{body}) // "Can not render mime-part of type <code>$_->{head}{content_maintype}/$_->{head}{content_subtype}</code>."
- </div>
-% }
-% elsif (lc $_->{head}{content_disposition} eq 'attachment') {
- <p>
- Attachment <%= $_->{head}{filename} %> of type
-%= to_mime_type $_->{head}
- </p>
-% }
-% else {
-% die "unknown Content-Disposition '$_->{head}{content_disposition}'"
-% }
-% }
-% }
-% elsif ($msg->{head}{mime}{content_maintype} eq 'message') {
-% die "not implemented" unless $msg->{head}{mime}{content_subtype} eq 'rfc822';
- <p>not implemented</p>
-% }
-% else {
- <div class="jwm-mail-body <%= $content_type eq 'text/plain' ? 'jwm-mail-body-text-plain' : '' %>" >
-%== mime_render($content_type, $body) // $content_type
- </div>
-% }
-
- </div>
-% end
-
-
<div class="jwm-base">
<h1>Read Mail</h1>
-%= $format_mail->($msg)
+%= $c->render_mail->format_mail($msg)
<nav>
<a href="javascript:history.back()" class="pure-button">