summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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">