summaryrefslogtreecommitdiff
path: root/lib/JWebmail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/JWebmail')
-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
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.
+