summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.md12
-rw-r--r--MANIFEST3
-rw-r--r--Makefile.PL1
-rw-r--r--README.md84
-rw-r--r--jwebmail.development.toml12
-rw-r--r--lib/JWebmail.pm46
-rw-r--r--lib/JWebmail/Controller/Webmail.pm26
-rw-r--r--lib/JWebmail/Model/ReadMails/MockJSON.pm4
-rw-r--r--lib/JWebmail/Model/ReadMails/MockMaildir.pm46
-rw-r--r--lib/JWebmail/Model/ReadMails/QMailAuthuser.pm82
-rw-r--r--lib/JWebmail/Model/ReadMails/Role.pm31
-rw-r--r--lib/JWebmail/Plugin/Helper.pm44
-rwxr-xr-xscript/qmauth.pl273
-rwxr-xr-xscript/qmauth.py349
-rw-r--r--t/Extract.t49
-rw-r--r--t/Helper.t64
-rw-r--r--t/Webmail.t8
-rw-r--r--templates/headers/_display_bot_nav.html.ep18
-rw-r--r--templates/headers/_display_folders.html.ep8
-rw-r--r--templates/headers/_display_headers.html.ep15
-rw-r--r--templates/headers/_pagination1.html.ep10
-rw-r--r--templates/headers/_pagination2.html.ep10
-rw-r--r--templates/webmail/readmail.html.ep43
23 files changed, 873 insertions, 365 deletions
diff --git a/CHANGES.md b/CHANGES.md
index cbd16de..1e9d2c4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -134,7 +134,7 @@ Current v1.1.0
- [ ] moving mails to other folders
- [ ] creating new folders
- [ ] backend
-- [ ] specify protocol for backend interaction
+- [ ] specify protocol for backend interaction (RPC IDL)
- [ ] choose tool for validation
- [ ] cleanup README
- [ ] improve about page
@@ -151,15 +151,15 @@ Current v1.1.0
- [ ] advance toml config plugin
- [ ] add template support, maybe
- [ ] add config validation
-
-- [x] compute hmac on the client side
- - [x] better handling on form
- - [ ] better random numbers
- [ ] improve i18n
- [ ] add localization of dates and time
- [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
+
Future
------
- [ ] INV: wrong subject being shown
@@ -198,3 +198,5 @@ Future
- [ ] address book support
- [ ] add links on email addresses in header : click = add into address book
- [ ] read and write s3d only once per request ($c->on('finish'))
+- [ ] classless CSS framework
+- [ ] dark and light mode
diff --git a/MANIFEST b/MANIFEST
index 49c28e4..123ba61 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -14,6 +14,9 @@ lib/JWebmail/Model/WriteMails.pm
lib/JWebmail/Plugin/Helper.pm
lib/JWebmail/Plugin/TOMLConfig.pm
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/ServerSideSessionData.pm
t/Webmail.t
diff --git a/Makefile.PL b/Makefile.PL
index 312fb5a..4209f3b 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -17,6 +17,7 @@ WriteMakefile(
Class::Method::Modifiers => '2.13',
TOML::Tiny => '0.15',
namespace::clean => '0.27',
+ MIME::Words => '5.510',
},
EXE_FILES => ['script/jwebmail', 'script/qmauth.pl'],
test => {TESTS => 't/*.t'},
diff --git a/README.md b/README.md
index 06185ec..3ab120c 100644
--- a/README.md
+++ b/README.md
@@ -143,3 +143,87 @@ Process overview
The Webserver acts as a proxy to the Application Server.
The Extractor is a stateless process that reads mails from a source.
+
+## Extractor Interface Definition
+
+Stateless Service - `net.fehcom.JWebmail.QMAuth.Maildir.repository` v0.1.0
+
+ struct MailAddress {
+ display_name: unicode_text,
+ address: unicode_text,
+ }
+
+ struct TopHead {
+ byte_size: uint64,
+ unread: bool,
+ date_received: iso8601,
+ message_handle: unicode_text,
+ head: Head,
+ }
+
+ struct Head {
+ date: iso8601,
+
+ from: Array[MailAddress],
+ sender: Array[MailAddress],
+ reply_to: Array[MailAddress],
+
+ to: Array[MailAddress],
+ cc: Array[MailAddress],
+ bcc: Array[MailAddress],
+
+ subject: unicode_text,
+ comments: unicode_text,
+ keywords: unicode_text,
+
+ mime: MIMEHeader,
+ }
+
+ sturct MIMEHeader {
+ content_maintype: unicode_text,
+ content_subtype: unicode_text,
+ content_disposition: unicode_text,
+ filename: unicode_text,
+ }
+
+ struct MIMEPart {
+ head: MIMEHeader,
+ body: Body,
+ }
+
+ enum Body {
+ discrete(unicode_text),
+ multipart{
+ preamble: unicode_text,
+ parts: Array[MIMEPart],
+ epilogue: unicode_text,
+ },
+ message(Message),
+ }
+
+ // body is either a string (discrete), an array (multipart) or object (message)
+ // depending on the content type
+ struct Message {
+ head: Head,
+ body: Body,
+ }
+
+ struct CountResult {
+ byte_size: uint64,
+ total_mails: uint32,
+ unread_mails: uint32,
+ }
+
+ fn __start__(maildir_directory: filepath, os_user: unicode_text, mail_user: unicode_text)
+
+ fn list(subfolder: unicode_text, start: uint32, end: uint32, sort_by: unicode_text) -> Array[TopHead]
+
+ fn count(folder: unicode_text) -> CountResult
+
+ fn read(folder: unicode_text, mid: unicode_text) -> Message
+
+ fn folders() -> Array[unicode_text]
+
+ fn move(mid: unicode_text, from_folder: unicode_text, to_folder: unicode_text)
+
+ fn search(pattern: unicode_text, folder: unicode_text) -> Array[Message]
diff --git a/jwebmail.development.toml b/jwebmail.development.toml
index 7f58015..c4aa39c 100644
--- a/jwebmail.development.toml
+++ b/jwebmail.development.toml
@@ -1,3 +1,5 @@
+logpath = "log/"
+
[defaults]
scriptadmin = "me@example.com" # for complaints / support
@@ -6,15 +8,13 @@ default_language = "en"
directory = "lib/JWebmail/I18N"
# languages = ["en", "de"]
-[model.read.driver.devel.json]
-[model.read.driver.devel.maildir]
+[model.read.devel]
+driver = "JWebmail::Model::ReadMails::MockMaildir"
+#driver = "JWebmail::Model::ReadMails::MockJSON"
[model.write]
#sendmail = "/usr/sbin/sendmail"
-
-[development]
-read_mock = "JWebmail::Model::ReadMails::MockJSON" # JWebmail::Model::ReadMails::MockMaildir
-block_writes = 1
+devel.block_writes = 1
[session]
# secure sesssion [none, cram, s3d]
diff --git a/lib/JWebmail.pm b/lib/JWebmail.pm
index 1c66001..3abc78d 100644
--- a/lib/JWebmail.pm
+++ b/lib/JWebmail.pm
@@ -1,4 +1,4 @@
-package JWebmail v1.2.0;
+package JWebmail v1.2.1;
use Mojo::Base Mojolicious;
@@ -10,27 +10,18 @@ use JWebmail::Model::ReadMails::QMailAuthuser;
use JWebmail::Model::WriteMails;
-sub nest {
-}
sub validateConf {
my $self = shift;
my $conf = $self->config;
- my $v = $self->validator->validation->input($conf);
- $v->optional('secret', 'not_empty');
+ exists $conf->{session}{secure} or die;
+ grep(sub { $_ eq $conf->{session}{secure} }, qw(none cram s3d)) > 0 or die;
- $v->optional('i18n');
- $v->required('session')->required('secure')->in(qw(none cram s3d));
- $v->required('defaults')->required('scriptadmin')->like(qr/@/);
- my $dev = $v->optional('development');
- $dev->optional('read_mock', 'not_empty');
- $dev->optional('block_writes')->in(0, 1);
+ exists $conf->{defaults}{scriptadmin} or die;
+ $conf->{defaults}{scriptadmin} =~ /@/ or die;
- for ($v->failed->@*) {
- say "reasons for $_: " , $v->error($_)->@*;
- }
- $v->is_valid;
+ return 1;
}
@@ -39,15 +30,19 @@ sub startup {
$self->moniker('jwebmail');
- $self->log->path($self->home->child('log', $self->mode . '.log'));
-
# load plugins
push @{$self->plugins->namespaces}, 'JWebmail::Plugin';
$self->plugin('TOMLConfig');
- #die unless $self->validateConf;
+ $self->validateConf;
- $self->plugin('ServerSideSessionData');
+ if (my $logpath = $self->config('logpath')) {
+ $self->log->path($logpath . '/' . $self->mode . '.log');
+ }
+
+ if (fc $self->config->{session}{secure} eq fc 's3d') {
+ $self->plugin('ServerSideSessionData');
+ }
$self->plugin('Helper');
my $i18n_route = $self->plugin('I18N2', $self->config('i18n'));
@@ -57,21 +52,22 @@ sub startup {
# initialize models
my $read_mails = do {
if ($self->mode eq 'development') {
- my $cls = $self->config->{development}{read_mock};
+ my $cls = $self->config->{model}{read}{devel}{driver};
eval { load $cls; 1 } || die "Issue for module $cls with: $@";
- $cls->new();
+ $cls->new(($self->config->{model}{read}{devel} // {})->%*)
}
else {
JWebmail::Model::ReadMails::QMailAuthuser->new(
- logfile => $self->home->child('log', 'extract.log'),
- );
+ ($self->config->{model}{read}{prod} // {})->%*
+ )
}
};
die "given class @{[ ref $read_mails ]} does not ReadMails"
unless $read_mails->DOES('JWebmail::Model::ReadMails::Role');
$self->helper(users => sub { $read_mails });
$self->helper(send_mail => sub { my ($c, $mail) = @_; JWebmail::Model::WriteMails::sendmail($mail) });
- $JWebmail::Model::WriteMails::Block_Writes = 1 if $self->mode eq 'development';
+ $JWebmail::Model::WriteMails::Block_Writes = 1
+ if $self->mode eq 'development' && $self->config->{model}{write}{devel}{block_writes};
$self->defaults(version => __PACKAGE__->VERSION);
@@ -98,7 +94,7 @@ sub route {
$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('/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 b094a43..2f71021 100644
--- a/lib/JWebmail/Controller/Webmail.pm
+++ b/lib/JWebmail/Controller/Webmail.pm
@@ -7,7 +7,7 @@ use List::Util 'first';
use Mojolicious::Types;
use constant {
- S_USER => 'user', # Key for user name in active session
+ S_USER => 'user', # Key for user name in active session
ST_AUTH => 'auth',
};
@@ -134,7 +134,7 @@ sub displayheaders {
my $folders = _time { $self->users->folders($auth) } $self, 'user folders';
- unless ( $self->stash('folder') ~~ $folders ) {
+ unless ( !$self->stash('folder') || $self->stash('folder') ~~ $folders ) {
$self->render(template => 'error',
status => 404,
error => $self->l('no_folder'),
@@ -153,9 +153,9 @@ sub displayheaders {
return;
}
- my ($total_byte_size, $cnt, $new) = _time { $self->users->count($auth, $self->stash('folder')) } $self, 'user count';
+ my $count = _time { $self->users->count($auth, $self->stash('folder')) } $self, 'user count';
- my ($start, $end) = $self->paginate($cnt);
+ my ($start, $end) = $self->paginate($count->{total_mails});
$self->timing->begin('user_headers');
my $headers = do {
@@ -178,10 +178,10 @@ sub displayheaders {
$self->app->log->debug(sprintf("Reading user headers took %fs", $elapsed));
$self->stash(
- msgs => $headers,
- mail_folders => $folders,
- total_size => $total_byte_size,
- total_new_mails => $new,
+ msgs => $headers,
+ mail_folders => $folders,
+ total_size => $count->{byte_size},
+ total_new_mails => $count->{unread_mails},
);
}
@@ -194,7 +194,7 @@ sub readmail {
my $auth = $self->stash(ST_AUTH);
my $mail;
- my $ok = eval { $mail = $self->users->show($auth, $mid); 1 };
+ my $ok = eval { $mail = $self->users->show($auth, '', $mid); 1 };
if (!$ok) {
my $err = $@;
if ($err =~ m/unkown mail-id|no such message/) {
@@ -210,11 +210,11 @@ sub readmail {
return if $v->has_error;
if ($type) {
- if ($mail->{head}{content_type} =~ '^multipart/') {
- my $content = first {$_->{head}{content_type} =~ $type} @{ $mail->{body} };
+ if ($mail->{head}{mime}{content_maintype} eq 'multipart') {
+ my $content = first {$_->{head}{mime}{content_subtype} eq $type} @{ $mail->{body} };
$self->render(text => $content->{body});
}
- elsif ($mail->{head}{content_type} =~ $type) {
+ elsif ($mail->{head}{mime}{content_subtype} eq $type) {
$self->render(text => $mail->{body}) ;
}
else {
@@ -297,7 +297,7 @@ sub move {
no warnings 'experimental::smartmatch';
die "$folder not valid" unless $folder ~~ $folders;
- $self->users->move($auth, $_, $folder) for @$mm;
+ $self->users->move($auth, $_, '', $folder) for @$mm;
$self->flash(message => $self->l('succ_move'));
$self->res->code(303);
diff --git a/lib/JWebmail/Model/ReadMails/MockJSON.pm b/lib/JWebmail/Model/ReadMails/MockJSON.pm
index b90a630..6b3b6d2 100644
--- a/lib/JWebmail/Model/ReadMails/MockJSON.pm
+++ b/lib/JWebmail/Model/ReadMails/MockJSON.pm
@@ -14,8 +14,8 @@ use Role::Tiny::With;
use namespace::clean;
use constant {
- VALID_USER => 'mockjson@example.com',
- VALID_PW => 'vwxyz',
+ VALID_USER => 'mockjson@example.org',
+ VALID_PW => '12345',
};
with 'JWebmail::Model::ReadMails::Role';
diff --git a/lib/JWebmail/Model/ReadMails/MockMaildir.pm b/lib/JWebmail/Model/ReadMails/MockMaildir.pm
index 2df4fa9..9b1bb29 100644
--- a/lib/JWebmail/Model/ReadMails/MockMaildir.pm
+++ b/lib/JWebmail/Model/ReadMails/MockMaildir.pm
@@ -1,38 +1,59 @@
package JWebmail::Model::ReadMails::MockMaildir;
-use Mojo::Base JWebmail::Model::ReadMails::QMailAuthuser;
+use Mojo::Base 'JWebmail::Model::ReadMails::QMailAuthuser';
use Mojo::JSON 'decode_json';
+use Digest::HMAC_MD5 'hmac_md5_hex';
+
+
use constant {
- VALID_USER => 'me@mockmaildir.com',
+ VALID_USER => 'mockmaildir@example.org',
VALID_PW => '12345',
};
-
has user => sub { $ENV{USER} };
has maildir => 't/';
has extractor => 'perl';
-
our %EXTRACTORS = (
- perl => 'perl script/qmauth.pl',
- rust => 'extract/target/debug/jwebmail-extract',
+ perl => 'perl script/qmauth.pl',
+ python => 'python script/qmauth.py',
+ rust => 'extract/target/debug/jwebmail-extract',
);
+sub new {
+ my $cls = shift;
+ my %args = @_ == 1 ? %$_[0] : @_;
+
+ my $self = bless {%args}, ref $cls || $cls;
+ $self->user;
+ $self->maildir;
+
+ $self->next::method(prog => $EXTRACTORS{$self->extractor});
+ return $self;
+}
+
+
sub verify_user {
my $self = shift;
my $auth = shift;
- return $auth->{user} eq VALID_USER && $auth->{password} eq VALID_PW;
+ my $passwd = $auth->{password}->show_password;
+
+ if ($auth->{challenge}) {
+ return $auth->{user} eq VALID_USER &&
+ $passwd eq hmac_md5_hex($auth->{challenge}, VALID_PW);
+ }
+ else {
+ return $auth->{user} eq VALID_USER && $passwd eq VALID_PW;
+ }
}
sub build_and_run {
my $self = shift;
- my $auth = shift;
- my $mode = shift;
- my $args = 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));
@@ -51,10 +72,11 @@ sub build_and_run {
if (my $err = $@) { $resp = {error => "decoding error '$err'"}; $rc ||= 1; };
}
elsif ($rc) {
- $resp = {error => "qmail-authuser returned code: $rc"};
+ $resp = {error => "qmauth returned code: $rc"};
}
- die "error $resp" if $rc;
+ local $" = ', ';
+ die "error @{[%$resp]}" if $rc;
return $resp;
}
diff --git a/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm b/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm
index 39d8ab6..956c137 100644
--- a/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm
+++ b/lib/JWebmail/Model/ReadMails/QMailAuthuser.pm
@@ -8,8 +8,8 @@ use File::Basename 'fileparse';
use IPC::Open2;
use JSON::PP 'decode_json';
use Params::Check 'check';
-use Scalar::Util 'blessed';
use Role::Tiny::With;
+use Scalar::Util 'blessed';
use namespace::clean;
with 'JWebmail::Model::ReadMails::Role';
@@ -51,7 +51,10 @@ package JWebmail::Model::ReadMails::QMailAuthuser::Error {
my $cls = shift;
my $msg = shift;
- die $cls->new($msg, @_)->_trace;
+ my $self = $cls->new($msg, @_);
+ $self->_trace;
+
+ die $self;
}
# taken from Mojo::Exception
@@ -70,12 +73,12 @@ package JWebmail::Model::ReadMails::QMailAuthuser::Error {
my $QMailAuthuserCheck = {
- user => {defined => 1, required => 1},
- maildir => {defined => 1, required => 1},
- prog => {defined => 1, required => 1},
- prefix => {defined => 1, default => ''},
- qmail_dir => {defined => 1, default => '/var/qmail/'},
- logfile => {defined => 1, default => '/dev/null'},
+ user => {required => 1},
+ maildir => {required => 1},
+ prog => {required => 1},
+ prefix => {default => ''},
+ qmail_dir => {default => '/var/qmail/'},
+ logfile => {default => '/dev/null'},
};
sub new {
@@ -86,8 +89,12 @@ sub new {
$self = {%$cls, %$self};
$cls = $pkg;
}
- $self = check($QMailAuthuserCheck, $self, 1) || die;
- return bless $self, $cls;
+ local $Params::Check::ALLOW_UNKNOWN = 1;
+ local $Params::Check::ONLY_ALLOW_DEFINED = 1;
+ local $Params::Check::WARNINGS_FATAL = 1;
+ my $s = check($QMailAuthuserCheck, $self)
+ or die __PACKAGE__ . " creation failed!";
+ return bless $s, $cls;
}
@@ -95,8 +102,8 @@ sub verify_user {
my $self = shift;
my $auth = shift;
- eval { $self->build_and_run($auth, 'auth'); 1 }
- or do {
+ return eval { $self->build_and_run($auth, 'auth'); 1 }
+ || do {
my $e = $@;
my $rc = eval { $e->data->{return_code} };
if ($rc == 1) {
@@ -115,22 +122,21 @@ sub read_headers_for {
my %h = @_;
my ($folder, $start, $end, $sort) = @h{qw(folder start end sort)};
- return $self->build_and_run($auth, 'list', [$start, $end, $sort, $folder]);
+ return $self->build_and_run($auth, 'list', [$folder, $start, $end, $sort]);
}
sub count {
my $self = shift;
my ($auth, $folder) = @_;
- my $resp = $self->build_and_run($auth, 'count', [$folder]);
- return ($resp->{size}, $resp->{count}, $resp->{new});
+ return $self->build_and_run($auth, 'count', [$folder]);
}
sub show {
my $self = shift;
- my ($auth, $mid) = @_;
+ my ($auth, $folder, $mid) = @_;
- return $self->build_and_run($auth, 'read-mail', [$mid]);
+ return $self->build_and_run($auth, 'read', [$folder, $mid]);
}
sub search {
@@ -144,23 +150,23 @@ sub folders {
my $self = shift;
my ($auth) = @_;
- return $self->build_and_run($auth, 'folders');
+ my $res = $self->build_and_run($auth, 'folders');
+ unshift @$res, '' if ref $res eq 'ARRAY';
+ return $res;
}
sub move {
my $self = shift;
- my ($auth, $mid, $folder) = @_;
+ my ($auth, $mid, $from_f, $to_f) = @_;
- my $_resp = $self->build_and_run($auth, 'move', [$mid, $folder]);
+ my $_resp = $self->build_and_run($auth, 'move', [$mid, $from_f, $to_f]);
return 1;
}
sub build_arg {
my $self = shift;
- my $user_mail_addr = shift;
- my $mode = shift;
- my $args = shift || [];
+ my ($user_mail_addr, $mode, $args) = @_;
return $self->{qmail_dir} . "/bin/qmail-authuser true 3<&0"
if $mode eq 'auth';
@@ -169,15 +175,14 @@ 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 = shift;
- my $exec = shift;
+ my ($auth, $exec) = @_;
my $pid = open2(my $reader, my $writer, $exec)
or die 'failed to create subprocess';
@@ -190,18 +195,28 @@ sub execute {
binmode $reader, ':encoding(UTF-8)';
my $input = <$reader>;
- close $reader # waits for the child to finish
+ close $reader
or die 'closing read pipe failed';
+ waitpid $pid, 0;
my $rc = $? >> 8;
my $resp;
if ($rc == 3 || $rc == 0) {
eval { $resp = decode_json $input; 1 }
- or $resp = {info => "error decoding response", response => $input, cause => $@, return_code => $rc};
+ or $resp = {
+ info => "error decoding response",
+ response => $input,
+ cause => $@,
+ return_code => $rc,
+ };
}
elsif ($rc) {
- $resp = {info => "got unsuccessful return code by qmail-authuser", return_code => $rc, response => $input};
+ $resp = {
+ info => "got unsuccessful return code by qmail-authuser",
+ return_code => $rc,
+ response => $input,
+ };
}
return ($resp, $rc);
@@ -209,15 +224,14 @@ sub execute {
sub build_and_run {
my $self = shift;
- my $auth = shift;
- my $mode = shift;
- my $args = shift;
+ my ($auth, $mode, $args) = @_;
my $exec = $self->build_arg($auth->{user}, $mode, $args);
my ($resp, $rc) = $self->execute($auth, $exec);
if ($rc) {
- JWebmail::Model::ReadMails::QMailAuthuser::Error->throw("qmail-authuser connection error", $resp);
+ JWebmail::Model::ReadMails::QMailAuthuser::Error->throw(
+ "qmail-authuser connection error", $resp);
}
return $resp;
}
@@ -291,5 +305,3 @@ Challenge when using cram
=head1 SEE ALSO
L<JWebmail::Model::ReadMails>, L<JWebmail::Model::Driver::QMailAuthuser::Extract>
-
-=cut
diff --git a/lib/JWebmail/Model/ReadMails/Role.pm b/lib/JWebmail/Model/ReadMails/Role.pm
index 1f4390b..d6fa1e5 100644
--- a/lib/JWebmail/Model/ReadMails/Role.pm
+++ b/lib/JWebmail/Model/ReadMails/Role.pm
@@ -1,6 +1,6 @@
package JWebmail::Model::ReadMails::Role;
-use Params::Check 'check';
+use Params::Check qw(check last_error);
use Mojo::Base -role; # load after imports
@@ -18,13 +18,15 @@ package JWebmail::Model::ReadMails::Role::Shadow {
sub Auth {
shift;
state $AuthCheck = {
- user => {defined => 1, required => 1},
- password => {defined => 1, required => 1},
+ user => {required => 1, defined => 1},
+ password => {required => 1, defined => 1},
challenge => {},
};
my $self = @_ == 1 ? $_[0] : {@_};
- my $res = check($AuthCheck, $self, 0) || die Params::Check::last_error;
+ local $Params::Check::WARNINGS_FATAL = 1;
+ my $res = check($AuthCheck, $self, 0)
+ or die 'Auth creation failed! ' . last_error;
$res->{password} = JWebmail::Model::ReadMails::Role::Shadow->new($res->{password});
return $res;
}
@@ -36,14 +38,14 @@ my @methods = (
# ^ throws exception
# ^type throws exception of type
# Read operations
- 'verify_user', # auth:Auth -> :truthy
- 'read_headers_for', # auth:Auth, *folder='', *start=0, *end=24, *sort='date' -> ^ :hashref
'count', # auth:Auth, folder -> ^ size:int count:int new:int
- 'show', # auth:Auth, mid -> ^ :hashref
- 'search', # auth:Auth, pattern, folder -> ^ :hashref
'folders', # auth:Auth -> ^ :arrayref
+ 'verify_user', # auth:Auth -> :truthy
# Write operations
'move', # auth:Auth, mid, folder -> ^ 1
+ 'read_headers_for', # auth:Auth, *folder='', *start=0, *end=24, *sort='date' -> ^ :hashref
+ 'search', # auth:Auth, pattern, folder -> ^ :hashref
+ 'show', # auth:Auth, mid -> ^ :hashref
);
requires(@methods);
@@ -66,13 +68,14 @@ around read_headers_for => sub {
my $args = {@_};
state $ArgsCheck = {
- start => {default => 0},
- end => {default => 24},
- sort => {default => 'date'},
+ start => {required => 1},
+ end => {required => 1},
+ sort => {default => ''},
folder => {default => ''},
};
- $orig->($self, $auth, %{ check($ArgsCheck, $args, 1) })
+ local $Params::Check::ONLY_ALLOW_DEFINED = 1;
+ $orig->($self, $auth, %{ check($ArgsCheck, $args, 0) or die last_error })
};
@@ -107,7 +110,7 @@ Provides bundeled information on a subset of mails of a mailbox.
Can be sorted and of varying size.
Arguments:
- start..end inclusive 0 based range
+ start..end half open 0 based range
=head2 count
@@ -150,5 +153,3 @@ Optinal challange for when you use cram authentication.
=head1 SEE ALSO
L<JWebmail::Model::ReadMails::QMailAuthuser>, L<JWebmail::Model::ReadMails::Mock>, L<JWebmail>
-
-=cut
diff --git a/lib/JWebmail/Plugin/Helper.pm b/lib/JWebmail/Plugin/Helper.pm
index ad5c8ad..c00ef0e 100644
--- a/lib/JWebmail/Plugin/Helper.pm
+++ b/lib/JWebmail/Plugin/Helper.pm
@@ -9,7 +9,6 @@ use POSIX qw(floor round log ceil);
use Mojo::Util qw(encode decode b64_encode b64_decode xml_escape);
use constant TRUE_RANDOM => eval { require Crypt::URandom; Crypt::URandom->import('urandom'); 1 };
-use constant HMAC_MD5 => eval { require Digest::HMAC_MD5; Digest::HMAC_MD5->import('hmac_md5'); 1 };
### filter and checks for mojo validator
@@ -160,8 +159,6 @@ sub session_passwd {
my ($c, $passwd, $challenge) = @_;
my $secAlg = $c->config->{session}{secure};
- die "you need to install Digest::HMAC_MD5 for cram to work"
- if !HMAC_MD5 && $secAlg eq 'cram';
warn_crypt($c);
if (defined $passwd) { # set
@@ -234,11 +231,9 @@ sub _paginate {
my %args = @_;
my $first_item = $args{first_item};
- my $page_size = $args{page_size} || 1;
+ my $page_size = $args{page_size};
my $total_items = $args{total_items};
- my $first_item1 = $total_items ? $first_item+1 : 0;
-
my $current_page = ceil($first_item/$page_size);
my $total_pages = ceil($total_items/$page_size);
@@ -246,23 +241,29 @@ sub _paginate {
my $page_ = shift;
return [0, 0] unless $total_items;
$page_ = _clamp(0, $page_, $total_pages-1);
- [_clamp(1, $page_*$page_size + 1, $total_items), _clamp(1, ($page_+1)*$page_size, $total_items)]
+ [_clamp(0, $page_*$page_size, $total_items-1), _clamp(0, ($page_+1)*$page_size, $total_items)]
};
- return (
- first_item => $first_item1,
- last_item => _clamp($first_item1, $first_item + $page_size, $total_items),
+ my %ret = (
total_items => $total_items,
page_size => $page_size,
total_pages => $total_pages,
- current_page => $current_page + 1,
+ current_page => $current_page,
first_page => $page->(0),
prev_page => $page->($current_page-1),
+ this_page => $page->($current_page),
next_page => $page->($current_page+1),
last_page => $page->($total_pages-1),
);
+
+ if ($total_items) {
+ $ret{first_item} = $first_item;
+ $ret{last_item} = _clamp($first_item, $first_item+$page_size-1, $total_items-1);
+ }
+
+ return %ret;
}
sub paginate {
@@ -274,9 +275,13 @@ sub paginate {
my $psize = $v->optional('page_size')->num(1, undef)->param // 50;
$start = _clamp(0, $start, max($count-1, 0));
- my $end = _clamp($start, $start+$psize-1, max($count-1, 0));
+ my $end = _clamp($start, $start+$psize, max($count, 0));
- $c->stash(_paginate(first_item => $start, page_size => $psize, total_items => $count));
+ $c->stash(_paginate(
+ first_item => int($start/$psize)*$psize,
+ page_size => $psize,
+ total_items => $count,
+ ));
return $start, $end;
}
@@ -435,16 +440,21 @@ Currently the following modes are supported:
=item none
-password is plainly stored in session cookie
+The password is plainly stored in session cookie.
+The cookie is stored on the client side and send with every request.
=item cram
-challenge response authentication mechanism uses the C<< $app->secret->[0] >> as nonce.
-This is optional if Digest::HMAC_MD5 is installed.
+A nonce is send to the client and the cram_md5 is generated there via js
+and crypto-js.
+This is vulnurable to replay attacks as the nonce is not invalidated ever.
=item s3d
-data is stored on the server. Additionally the password is encrypted by an one-time-pad that is stored in the user cookie.
+The password is stored on the server. Additionally the password is encrypted
+by an one-time-pad that is stored in the users cookie.
+This is vulnurable to replay attacks during an active session.
+On log-in it is transfered plainly.
=back
diff --git a/script/qmauth.pl b/script/qmauth.pl
index 3ecadef..000eaa0 100755
--- a/script/qmauth.pl
+++ b/script/qmauth.pl
@@ -5,54 +5,59 @@ use v5.18;
use warnings;
use utf8;
-use POSIX ();
-use JSON::PP;
use Carp;
-use List::Util 'min';
use Encode v2.88 'decode';
+use JSON::PP;
+use List::Util 'min';
+use POSIX 'setuid';
#use open IO => ':encoding(UTF-8)', ':std';
-no warnings 'experimental::smartmatch';
-use Mail::Box::Manager;
+use Mail::Box::Maildir;
-use constant ROOT_MAILDIR => '.';
+
+package JWebmail::QMAuth::Message::Head::Complete {
+ use parent 'Mail::Message::Head::Complete';
+
+ use File::Basename;
+
+ sub createMessageId {
+ my $self = shift;
+
+ my ($mid) = scalar(fileparse($self->message->filename)) =~ /(.+):/;
+ return $mid || $self->SUPER::createMessageId;
+ }
+}
sub main {
- my ($maildir) = shift(@ARGV) =~ m/(.*)/;
- my ($su) = shift(@ARGV) =~ m/(.*)/;
- my ($user) = shift(@ARGV) =~ m/([[:alpha:]]+)/;
- my $mode = shift @ARGV; _ok($mode =~ m/([[:alpha:]-]{1,20})/);
- my @args = @ARGV;
+ my ($maildir, $su, $user, $mode, @args) = @ARGV;
delete $ENV{PATH};
my $netfehcom_uid = getpwnam($su);
- #$> = $netfehcom_uid;
die "won't stay as root" if $netfehcom_uid == 0;
- POSIX::setuid($netfehcom_uid);
+ setuid($netfehcom_uid);
if ($!) {
warn 'error setting uid';
exit(1);
}
- my $folder = Mail::Box::Manager->new->open(
- folder => "$maildir/$user/",
- type => 'maildir',
- access => 'rw',
+ my $folder = Mail::Box::Maildir->new(
+ folder => "$maildir/$user/",
+ type => 'maildir',
+ access => 'rw',
+ head_type => 'JWebmail::QMAuth::Message::Head::Complete',
);
my $reply = do {
- given ($mode) {
- when('list') { list($folder, @args) }
- when('read-mail') { read_mail($folder, @args) }
- when('count') { count_messages($folder, @args) }
- when('search') { search($folder, @args) }
- when('folders') { folders($folder, @args) }
- when('move') { move($folder, @args) }
- default { {error => 'unkown mode', mode => $mode} }
- }
+ if ($mode eq 'list') { list($folder, @args) }
+ elsif ($mode eq 'read') { read_mail($folder, @args) }
+ elsif ($mode eq 'count') { count_messages($folder, @args) }
+ elsif ($mode eq 'search') { search($folder, @args) }
+ elsif ($mode eq 'folders') { folders($folder, @args) }
+ elsif ($mode eq 'move') { move($folder, @args) }
+ else { {error => 'unknown mode', mode => $mode} }
};
$folder->close;
@@ -64,22 +69,26 @@ sub main {
sub _sort_mails {
- my $sort = shift // '';
- my $reverse = 1;
+ my ($sort) = @_;
- if ($sort =~ m/^!/) {
- $reverse = -1;
+ my $reverse = '';
+ if ($sort =~ /^!/) {
+ $reverse = 1;
$sort = substr $sort, 1;
}
- given ($sort) {
- when ('date') { return sub { ($a->timestamp <=> $b->timestamp) * $reverse } }
- when ('sender') { return sub { ($a->from->[0] cmp $b->from->[0]) * $reverse } }
- when ('subject') { return sub { ($a->subject cmp $b->subject) * $reverse } }
- when ('size') { return sub { ($a->size <=> $b->size) * $reverse } }
- when ('') { return sub { ($a->timestamp <=> $b->timestamp) * $reverse } }
- default { warn "unkown sort-verb '$sort'"; return sub { ($a->timestamp <=> $b->timestamp) * $reverse } }
- }
+ my $sortsub = do {
+ if ($sort eq 'date') { sub { $a->timestamp <=> $b->timestamp } }
+ elsif ($sort eq 'sender') { sub { $a->from->[0] cmp $b->from->[0] } }
+ elsif ($sort eq 'subject') { sub { $a->subject cmp $b->subject } }
+ elsif ($sort eq 'size') { sub { $a->size <=> $b->size } }
+ elsif ($sort eq '') { sub { $a->timestamp <=> $b->timestamp } }
+ else {
+ warn "unknown sort-verb '$sort'";
+ sub { $a->timestamp <=> $b->timestamp }
+ }
+ };
+ return $reverse ? sub { $sortsub->() * -1 } : $sortsub;
}
@@ -91,61 +100,79 @@ sub _ok {
}
+sub _get_mime_head_info {
+ my ($msg) = @_;
+
+ return {
+ content_maintype => $msg->body->mimeType->mediaType,
+ content_subtype => $msg->body->mimeType->subType,
+ content_disposition => ''.$msg->body->disposition,
+ filename => $msg->body->dispositionFilename,
+ };
+}
+
+
+sub _get_head_info {
+ my ($msg) = @_;
+
+ return {
+ date => _iso8601_utc($msg->timestamp),
+
+ from => _addresses($msg->from),
+ sender => _addresses($msg->sender),
+
+ to => _addresses($msg->to),
+ cc => _addresses($msg->cc),
+ bcc => _addresses($msg->bcc),
+
+ subject => decode('MIME-Header', $msg->subject),
+ comments => $msg->get('comments'),
+ keywords => $msg->get('keywords'),
+
+ mime => _get_mime_head_info($msg),
+ };
+}
+
+
sub list {
- my ($f, $start, $end, $sortby, $folder) = @_;
- $folder = ".$folder";
+ my ($f, $folder, $start, $end, $sortby) = @_;
- _ok($start =~ m/^\d+$/);
- _ok($end =~ m/^\d+$/);
+ _ok($start =~ /^\d+$/aa);
+ _ok($end =~ /^\d+$/aa);
_ok(0 <= $start && $start <= $end);
- _ok($sortby =~ m/^(!?\w+|\w*)$/n);
- _ok($folder ~~ [$f->listSubFolders, ROOT_MAILDIR]);
+ _ok($sortby =~ /^(?:!?\w+|)$/aa);
+ _ok(!$folder || grep { $_ eq ".$folder" } $f->listSubFolders);
- $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR;
+ $f = $f->openSubFolder(".$folder") if $folder;
return [] if $start == $end;
my $sref = _sort_mails($sortby);
my @msgs = $f->messages;
@msgs = sort { &$sref } @msgs;
- @msgs = @msgs[$start..min($#msgs, $end)];
-
- my @msgs2;
-
- for my $msg (@msgs) {
- my $msg2 = {
- mid => $msg->messageId,
- size => $msg->size,
- new => $msg->label('seen'),
- head => {
- subject => decode('MIME-Header', $msg->subject),
- from => _addresses($msg->from),
- to => _addresses($msg->to),
- cc => _addresses($msg->cc),
- bcc => _addresses($msg->bcc),
- date => _iso8601_utc($msg->timestamp),
- content_type => ''.$msg->contentType,
- },
- };
- push @msgs2, $msg2;
- }
-
- return \@msgs2;
+ @msgs = @msgs[$start..min($#msgs, $end-1)];
+
+ return [map {
+ {
+ byte_size => $_->size,
+ unread => !$_->label('seen'),
+ date_received => $_->guessTimestamp,
+ message_handle => $_->messageId,
+ head => _get_head_info($_),
+ }
+ } @msgs];
}
sub count_messages {
my ($f, $folder) = @_;
- $folder = ".$folder";
-
- _ok($folder ~~ [$f->listSubFolders, ROOT_MAILDIR]);
- $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR;
+ $f = $f->openSubFolder(".$folder") if $folder;
return {
- count => scalar($f->messages('ALL')),
- size => $f->size,
- new => scalar $f->messages('!seen'),
+ total_mails => scalar $f->messages('ALL'),
+ byte_size => $f->size,
+ unread_mails => scalar $f->messages('!seen'),
}
}
@@ -160,46 +187,55 @@ sub _iso8601_utc {
sub _unquote { my $x = shift; [$x =~ m/"(.*?)"(?<!\\)/]->[0] || $x }
sub _addresses {
+ return undef unless @_;
[map { {address => $_->address, name => _unquote(decode('MIME-Header', $_->phrase))} } @_]
}
+sub _get_body {
+ my ($msg) = @_;
+
+ if ($msg->isNested) {
+ my $nested = $msg->body->nested;
+ return {
+ head => _get_head_info($nested),
+ body => _get_body($nested),
+ };
+ }
+ elsif ($msg->isMultipart) {
+ return {
+ preamble => ''.$msg->body->preamble,
+ parts => [map { { head => _get_mime_head_info($_), body => _get_body($_) } } $msg->parts],
+ epilogue => $msg->body->epilogue,
+ }
+ }
+ else {
+ return ''.$msg->decoded;
+ }
+}
+
+
sub read_mail {
- my ($folder, $mid) = @_;
+ my ($f, $folder, $mid) = @_;
+
+ $f = $folder->openSubFolder(".$folder") if $folder;
- my $msg = $folder->find($mid);
+ my $msg = $f->find($mid);
return {error => 'no such message', mid => $mid} unless $msg;
return {
- size => $msg->size,
- head => {
- subject => decode('MIME-Header', $msg->subject),
- from => _addresses($msg->from),
- to => _addresses($msg->to),
- cc => _addresses($msg->cc),
- bcc => _addresses($msg->bcc),
- date => _iso8601_utc($msg->timestamp),
- content_type => ''. $msg->contentType,
- },
- body => do {
- if ($msg->isMultipart) {
- [map {{type => ''. $_->contentType, val => '' . $_->decoded}} $msg->body->parts]
- }
- else {
- '' . $msg->body->decoded
- }
- },
+ head => _get_head_info($msg),
+ body => _get_body($msg),
}
}
sub search {
my ($f, $search_pattern, $folder) = @_;
- $folder = ".$folder";
- $f = $f->openSubFolder($folder) if $folder ne ROOT_MAILDIR;
+ $f = $f->openSubFolder(".$folder") if $folder;
my @msgs = $f->messages(sub {
- my $m = shift;
+ my ($m) = @_;
return scalar(grep { $_->decoded =~ /$search_pattern/ || (decode('MIME-Header', $_->subject)) =~ /$search_pattern/ } $m->body->parts)
if $m->isMultipart;
@@ -209,17 +245,8 @@ sub search {
my @msgs2;
for my $msg (@msgs) {
my $msg2 = {
- size => $msg->size,
- mid => $msg->messageId,
- head => {
- subject => decode('MIME-Header', $msg->subject),
- from => _addresses($msg->from),
- to => _addresses($msg->to),
- cc => _addresses($msg->cc),
- bcc => _addresses($msg->bcc),
- date => _iso8601_utc($msg->timestamp),
- content_type => ''. $msg->contentType,
- },
+ head => _get_head_info($msg),
+ body => '',
};
push @msgs2, $msg2;
}
@@ -231,17 +258,18 @@ sub search {
sub folders {
my $f = shift;
- return [grep { $_ =~ m/^\./ && $_ =~ s/\.// } $f->listSubFolders];
+ return [map { s/^\.//r } $f->listSubFolders(check => 1)];
}
sub move {
- my ($f, $mid, $dst) = @_;
- $dst = ".$dst";
+ my ($f, $mid, $from, $to) = @_;
- _ok($dst ~~ [$f->listSubFolders, ROOT_MAILDIR]);
+ $f = $f->openSubFolder(".$from") if $from;
- $f->moveMessage($dst, $dst->find($mid));
+ $f->find($mid)->moveTo($to ? ".$to" : $to);
+
+ return 1;
}
@@ -269,20 +297,7 @@ a succsessful login.
Input directives are provided as command line arguments.
Output is delivered via STDOUT and log information via STDERR.
-=head1 ARGUMENTS
-
- prog <maildir> <system-user> <mail-user> <mode> <args...>
-
-=head2 Modes
-
- list <start> <end> <sort-by> <folder>
- count <folder>
- read-mail <mid>
- search <pattern> <folder>
- folders
- move <mid> <dst-folder>
-
-All arguments must be supplied for a given mode even if empty (as '').
+For the implemented interface see the README file.
=head1 DEPENDENCIES
@@ -290,6 +305,6 @@ Currently Mail::Box::Manager does all the hard work.
=head1 SEE ALSO
-L<JWebmail::Model::Driver::QMailAuthuser>
+L<JWebmail::Model::ReadMails::QMailAuthuser>
=cut
diff --git a/script/qmauth.py b/script/qmauth.py
new file mode 100755
index 0000000..7803483
--- /dev/null
+++ b/script/qmauth.py
@@ -0,0 +1,349 @@
+#!/usr/bin/env python3
+
+"""qmauth.py
+
+Extract delivers information about emails from a maildir.
+Runs with elevated privileges.
+
+This program is started by qmail-authuser with elevated privileges after
+a successful login.
+Input directives are provided as command line arguments.
+Output is delivered via STDOUT as json and log information via STDERR.
+
+Exit codes::
+
+ 1 reserved
+ 2 reserved
+ 3 operational error (error message in output)
+ 4 user error (no output)
+ 5 issue switching to user (no output)
+ 110 reserved
+ 111 reserved
+"""
+
+import json
+import logging
+import re
+
+from argparse import ArgumentParser
+from datetime import datetime
+from email import message_from_binary_file, policy
+from functools import cache
+from glob import glob
+from mailbox import Maildir, MaildirMessage
+from os import environ, getpid, path, setuid, stat
+from pathlib import Path
+from pwd import getpwnam
+from sys import exit as sysexit, stdout
+
+
+class MyMaildir(Maildir):
+
+ def __init__(self, dirname, *args, **kwargs):
+ self.__path = dirname
+ super().__init__(dirname, *args, **kwargs)
+
+ def get_filename(self, mid):
+ if mid not in self:
+ raise KeyError(mid)
+ p_cur = glob(path.join(self.__path, 'cur', mid + '*'))
+ 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")
+ return res[0]
+
+ def get_folder(self, folder):
+ # copy from internal implementation
+ return MyMaildir(
+ path.join(self._path, '.' + folder), factory=self._factory, create=False,
+ )
+
+
+@cache
+def _file_size(fname):
+ return stat(fname).st_size
+
+
+def _adr(addrs):
+ if addrs is None:
+ return None
+ return [
+ {'address': addr.addr_spec, 'display_name': addr.display_name}
+ for addr in addrs.addresses
+ ]
+
+
+def _get_rcv_time(mid):
+ idx = mid.find('.')
+ assert idx >= 0
+ return float(mid[:idx])
+
+
+def startup(maildir, su, user):
+
+ del environ['PATH']
+
+ netfehcom_uid = getpwnam(su).pw_uid
+ assert netfehcom_uid, "must be non root"
+ try:
+ setuid(netfehcom_uid)
+ except OSError:
+ logging.exception("error setting uid")
+ sysexit(5)
+
+ return MyMaildir(
+ maildir / user,
+ create=False,
+ factory=lambda x: MaildirMessage(
+ message_from_binary_file(x, policy=policy.default)
+ ),
+ )
+
+
+def _sort_by_sender(midmsg):
+ _, msg = midmsg
+
+ if len(addrs := msg['from'].addresses) == 1:
+ return addrs[0].addr_spec
+ else:
+ return msg['sender'].address.addr_spec
+
+
+def _sort_mails(f, sort):
+
+ reverse = False
+ if sort.startswith('!'):
+ reverse = True
+ sort = sort[1:]
+
+ by_rec_date = lambda midmsg: float(re.match(r"\d+\.\d+", midmsg[0], re.ASCII)[0])
+
+ if sort == 'date': keyfn = by_rec_date
+ elif sort == 'sender': keyfn = _sort_by_sender
+ elif sort == 'subject': keyfn = lambda midmsg: midmsg[1]['subject']
+ elif sort == 'size': keyfn = lambda midmsg: _file_size(f.get_filename(midmsg[0]))
+ elif sort == '': keyfn = by_rec_date
+ else:
+ logging.warning("unknown sort-verb %r", sort)
+ reverse = False
+ keyfn = by_rec_date
+
+ return keyfn, reverse
+
+
+def _get_mime_head_info(msg):
+ return {
+ 'content_maintype': msg.get_content_maintype(),
+ 'content_subtype': msg.get_content_subtype(),
+ 'content_disposition': msg.get_content_disposition(),
+ 'filename': msg.get_filename(),
+ }
+
+
+def _get_head_info(msg):
+ return {
+ 'date': msg['date'].datetime.isoformat(),
+
+ 'from': _adr(msg['from']),
+ 'sender': _adr(msg['sender']),
+ 'reply_to': _adr(msg['reply-to']),
+
+ 'to': _adr(msg['to']),
+ 'cc': _adr(msg['cc']),
+ 'bcc': _adr(msg['bcc']),
+
+ 'subject': msg['subject'],
+ 'comments': msg['comments'],
+ 'keywords': msg['keywords'],
+
+ 'mime': _get_mime_head_info(msg),
+ }
+
+
+def list_mails(f, start, end, sortby, folder):
+
+ assert 0 <= start <= end
+
+ if folder:
+ f = f.get_folder(folder)
+
+ if start == end:
+ return []
+
+ kfn, reverse = _sort_mails(f, sortby)
+ msgs = list(f.items())
+ msgs.sort(key=kfn, reverse=reverse)
+ msgs = msgs[start : min(len(msgs), end)]
+
+ return [
+ {
+ 'message_handle': mid,
+ 'byte_size': _file_size(f.get_filename(mid)),
+ 'unread': 'S' in msg.get_flags(),
+ 'date_received': datetime.fromtimestamp(_get_rcv_time(mid)).isoformat(),
+ 'head': _get_head_info(msg),
+ }
+ for mid, msg in msgs
+ ]
+
+
+def count_mails(f, subfolder):
+ if subfolder:
+ f = f.get_folder(subfolder)
+
+ return {
+ 'total_mails': len(f),
+ 'byte_size': sum(_file_size(f.get_filename(mid)) for mid in f.keys()),
+ 'unread_mails': len([1 for m in f if 'S' in m.get_flags()]),
+ }
+
+
+def _get_body(mail):
+ if not mail.is_multipart():
+ if mail.get_content_maintype() == 'text':
+ return mail.get_payload(decode=True).decode()
+ else:
+ return mail.get_payload()
+
+ if (mctype := mail.get_content_maintype()) == 'message':
+ mm = mail.get_payload()
+ assert len(mm) == 1
+ msg = mm[0]
+ return {
+ 'head': _get_head_info(msg),
+ 'body': msg.get_content(),
+ }
+ elif mctype == 'multipart':
+ return {
+ 'preamble': mail.preamble,
+ 'parts': [
+ {
+ 'head': _get_mime_head_info(part),
+ 'body': _get_body(part),
+ }
+ for part in mail.get_payload()
+ ],
+ 'epilogue': mail.epilogue,
+ }
+ else:
+ raise ValueError(f"unknown major content-type {mctype!r}")
+
+
+def read_mail(f, subfolder, mid):
+ if subfolder:
+ f = f.get_folder(subfolder)
+
+ msg = f[mid]
+ if not msg:
+ return {'error': "no such message", 'mid': mid}
+
+ return {
+ 'head': _get_head_info(msg),
+ 'body': _get_body(msg),
+ }
+
+
+def _matches(m, pattern):
+ if m.is_multipart():
+ return any(
+ 1
+ for part in m.body.parts
+ if re.search(pattern, part.decoded()) or re.search(pattern, part.subject)
+ )
+ return re.search(pattern, m.body.decoded()) or re.search(pattern, m.subject)
+
+
+def search_mails(f, pattern: str, subfolder: str):
+ if subfolder:
+ f = f.get_folder(subfolder)
+
+ return [
+ {
+ 'head': _get_head_info(msg),
+ 'body': _get_body(msg),
+ }
+ for msg in f.values()
+ if _matches(msg, pattern)
+ ]
+
+
+def folders(f):
+ return f.list_folders()
+
+
+def move_mail(f, mid, from_, to):
+ if from_:
+ f = f.get_folder(from_)
+
+ fname = Path(f.get_filename(mid))
+
+ assert to in f.list_folders()
+
+ sep = -2 if not from_ else -3
+
+ if to:
+ res = fname.parts[:sep] + ('.' + to,) + fname.parts[-2:]
+ else:
+ res = fname.parts[:sep] + fname.parts[-2:]
+
+ fname.rename(Path(*res))
+
+ return 1
+
+
+ap = ArgumentParser(allow_abbrev=False)
+ap.add_argument('maildir_path', type=Path)
+ap.add_argument('os_user')
+ap.add_argument('mail_user')
+sp = ap.add_subparsers(title='methods', required=True)
+
+sp_list = sp.add_parser('list')
+sp_list.add_argument('folder', metavar='subfolder')
+sp_list.add_argument('start', type=int)
+sp_list.add_argument('end', type=int)
+sp_list.add_argument('sortby', metavar='sort_by')
+sp_list.set_defaults(run=list_mails)
+
+sp_count = sp.add_parser('count')
+sp_count.add_argument('subfolder')
+sp_count.set_defaults(run=count_mails)
+
+sp_read = sp.add_parser('read')
+sp_read.add_argument('subfolder')
+sp_read.add_argument('mid', metavar='message')
+sp_read.set_defaults(run=read_mail)
+
+sp_folders = sp.add_parser('folders')
+sp_folders.set_defaults(run=folders)
+
+sp_move = sp.add_parser('move')
+sp_move.add_argument('mid', metavar='message')
+sp_move.add_argument('from_', metavar='from')
+sp_move.add_argument('to')
+sp_move.set_defaults(run=move_mail)
+
+sp_search = sp.add_parser('search')
+sp_search.add_argument('pattern')
+sp_search.add_argument('subfolder')
+sp_search.set_defaults(run=search_mails)
+
+
+if __name__ == '__main__':
+ try:
+ logging.basicConfig(
+ level='INFO',
+ format="%(levelname)s:"+str(getpid())+":%(message)s",
+ )
+ args = vars(ap.parse_args())
+ logging.debug("started with %s", args)
+ s = startup(args.pop('maildir_path'), args.pop('os_user'), args.pop('mail_user'))
+ 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 Exception:
+ logging.exception("qmauth.py error")
+ sysexit(4)
diff --git a/t/Extract.t b/t/Extract.t
index 7b1ddbf..3a18395 100644
--- a/t/Extract.t
+++ b/t/Extract.t
@@ -6,13 +6,13 @@ use Test::More;
use JSON::PP 'decode_json';
use List::Util 'min';
-no warnings 'experimental::smartmatch';
my $EXTRACT = {
- perl_mail_box => 'perl script/qmauth.pl ',
- rust_maildir => 'extract/target/debug/jwebmail-extract',
-}->{perl_mail_box};
+ perl_mail_box => 'perl script/qmauth.pl',
+ python_mailbox => 'python script/qmauth.py',
+ rust_maildir => 'extract/target/debug/jwebmail-extract',
+}->{python_mailbox};
my $MAILDIR = 't/';
my $SYS_USER = $ENV{USER};
my $MAIL_USER = 'maildir';
@@ -29,13 +29,15 @@ my $MAIL_COUNT = do {
};
subtest start => sub {
- my @res = `$PROG invalid`;
+ my @res = `$PROG invalid 2>/dev/null`;
- is($? >> 8, 3);
- is @res, 1;
- my $result = decode_json $res[0];
+ isnt $? >> 8, 0;
- ok($result->{error})
+ if ($? >> 8 == 3) {
+ is scalar(@res), 1;
+ my $result = decode_json @res;
+ ok exists $result->{error};
+ }
};
subtest folders => sub {
@@ -55,43 +57,44 @@ subtest count => sub {
is @res, 1;
my $result = decode_json $res[0];
- is($result->{count}, $MAIL_COUNT);
- #is($result->{new}, 0);
+ is($result->{total_mails}, $MAIL_COUNT);
};
subtest list => sub {
- my $s = "$PROG list 0 4 date ''";
+ my $s = "$PROG list '' 0 4 date";
my @res = `$s`;
is($? >> 8, 0);
is @res, 1;
my $result = decode_json $res[0];
- is(@$result, min($MAIL_COUNT, 5));
- ok($result->[0]{mid});
+ is(@$result, min($MAIL_COUNT, 4));
+ ok($result->[0]{message_handle});
ok($result->[0]{head}{from}) or diag $s;
ok($result->[0]{head}{to});
};
subtest read => sub {
- my @pre_res = `$PROG list 0 10 date ''`;
+ my $folder = '';
+ my @pre_res = `$PROG list '$folder' 0 10 ''`;
is($? >> 8, 0);
is @pre_res, 1;
my $pre_result = decode_json $pre_res[0];
- ok(my $mid = $pre_result->[0]{mid});
+ ok(my $mid = $pre_result->[0]{message_handle});
- my $s = "$PROG read-mail '$mid'";
- my @res = `$s`;
+ my @res = `$PROG read '$folder' '$mid'`;
- is($? >> 8, 0) or (diag $s, return);
+ is($? >> 8, 0, "read exit code") or (diag @res, return);
is @res, 1;
my $result = decode_json $res[0];
- is_deeply($result->{head}{from}, [{address => 'shipment-tracking@amazon.de', name => 'Amazon.de'}]);
- ok($result->{date_received});
- ok(index($result->{date_received}, '2019-02-22T10:06:54') != -1);
- like($result->{date_received}, qr'2019-02-22T10:06:54');
+ is_deeply(
+ $result->{head}{from},
+ [{address => 'xy@example.com', display_name => 'Moderator-Address'}],
+ );
+ ok(exists $result->{head}{date});
+ is($result->{head}{date}, '1994-03-22T13:34:51+00:00');
};
done_testing;
diff --git a/t/Helper.t b/t/Helper.t
index 99758a3..45d0e4f 100644
--- a/t/Helper.t
+++ b/t/Helper.t
@@ -85,73 +85,73 @@ subtest 'pagination' => sub {
%res = JWebmail::Plugin::Helper::_paginate(first_item => 0, page_size => 10, total_items => 55);
- is $res{first_item}, 1;
- is $res{last_item}, 10;
+ is $res{first_item}, 0;
+ is $res{last_item}, 9;
is $res{total_items}, 55;
is $res{page_size}, 10;
is $res{total_pages}, 6;
- is $res{current_page}, 1;
+ is $res{current_page}, 0;
- is_deeply $res{first_page}, [1, 10], 'first';
- is_deeply $res{prev_page}, [1, 10], 'prev';
- is_deeply $res{next_page}, [11, 20], 'next';
- is_deeply $res{last_page}, [51, 55], 'last';
+ is_deeply $res{first_page}, [0, 10], 'first';
+ is_deeply $res{prev_page}, [0, 10], 'prev';
+ is_deeply $res{next_page}, [10, 20], 'next';
+ is_deeply $res{last_page}, [50, 55], 'last';
%res = JWebmail::Plugin::Helper::_paginate(first_item => 10, page_size => 10, total_items => 55);
- is $res{first_item}, 11;
- is $res{last_item}, 20;
+ is $res{first_item}, 10;
+ is $res{last_item}, 19;
is $res{total_items}, 55;
is $res{page_size}, 10;
is $res{total_pages}, 6;
- is $res{current_page}, 2;
+ is $res{current_page}, 1;
- is_deeply $res{first_page}, [1, 10], 'first';
- is_deeply $res{prev_page}, [1, 10], 'prev';
- is_deeply $res{next_page}, [21, 30], 'next';
- is_deeply $res{last_page}, [51, 55], 'last';
+ is_deeply $res{first_page}, [0, 10], 'first';
+ is_deeply $res{prev_page}, [0, 10], 'prev';
+ is_deeply $res{next_page}, [20, 30], 'next';
+ is_deeply $res{last_page}, [50, 55], 'last';
%res = JWebmail::Plugin::Helper::_paginate(first_item => 20, page_size => 10, total_items => 55);
- is $res{first_item}, 21;
- is $res{last_item}, 30;
+ is $res{first_item}, 20;
+ is $res{last_item}, 29;
is $res{total_items}, 55;
is $res{page_size}, 10;
is $res{total_pages}, 6;
- is $res{current_page}, 3;
+ is $res{current_page}, 2;
- is_deeply $res{first_page}, [1, 10], 'first';
- is_deeply $res{prev_page}, [11, 20], 'prev';
- is_deeply $res{next_page}, [31, 40], 'next';
- is_deeply $res{last_page}, [51, 55], 'last';
+ is_deeply $res{first_page}, [0, 10], 'first';
+ is_deeply $res{prev_page}, [10, 20], 'prev';
+ is_deeply $res{next_page}, [30, 40], 'next';
+ is_deeply $res{last_page}, [50, 55], 'last';
%res = JWebmail::Plugin::Helper::_paginate(first_item => 50, page_size => 10, total_items => 55);
- is $res{first_item}, 51;
- is $res{last_item}, 55;
+ is $res{first_item}, 50;
+ is $res{last_item}, 54;
is $res{total_items}, 55;
is $res{page_size}, 10;
is $res{total_pages}, 6;
- is $res{current_page}, 6;
+ is $res{current_page}, 5;
- is_deeply $res{first_page}, [1, 10], 'first';
- is_deeply $res{prev_page}, [41, 50], 'prev';
- is_deeply $res{next_page}, [51, 55], 'next';
- is_deeply $res{last_page}, [51, 55], 'last';
+ is_deeply $res{first_page}, [0, 10], 'first';
+ is_deeply $res{prev_page}, [40, 50], 'prev';
+ is_deeply $res{next_page}, [50, 55], 'next';
+ is_deeply $res{last_page}, [50, 55], 'last';
%res = JWebmail::Plugin::Helper::_paginate(first_item => 0, page_size => 10, total_items => 0);
- is $res{first_item}, 0;
- is $res{last_item}, 0;
+ ok !defined $res{first_item};
+ ok !defined $res{last_item};
is $res{total_items}, 0;
is $res{page_size}, 10;
is $res{total_pages}, 0;
- is $res{current_page}, 1;
+ is $res{current_page}, 0;
is_deeply $res{first_page}, [0, 0], 'first';
is_deeply $res{prev_page}, [0, 0], 'prev';
@@ -181,4 +181,4 @@ subtest 'pagination' => sub {
};
-done_testing; \ No newline at end of file
+done_testing;
diff --git a/t/Webmail.t b/t/Webmail.t
index 9218fb1..3deb547 100644
--- a/t/Webmail.t
+++ b/t/Webmail.t
@@ -14,9 +14,11 @@ my $pw = JWebmail::Model::ReadMails::MockJSON::VALID_PW;
my $t = Test::Mojo->new('JWebmail', {
- development => { read_mock => 'JWebmail::Model::ReadMails::MockJSON', block_writes => 1 },
- i18n => { default_language => DEFAULT_LANGUAGE },
- session => { secure => 'none' },
+ model => { read => { devel => { driver => 'JWebmail::Model::ReadMails::MockJSON' }},
+ write => { devel => { block_writes => 1 }}},
+ i18n => { default_language => DEFAULT_LANGUAGE, directory => 'lib/JWebmail/I18N' },
+ session => { secure => 'none' },
+ defaults => { scriptadmin => 'test@example.org' },
});
$t->get_ok('/')->status_is(200);
diff --git a/templates/headers/_display_bot_nav.html.ep b/templates/headers/_display_bot_nav.html.ep
index 5a58b77..7be2832 100644
--- a/templates/headers/_display_bot_nav.html.ep
+++ b/templates/headers/_display_bot_nav.html.ep
@@ -10,14 +10,16 @@
</div>
<div class="pure-u-1-1 pure-u-md-1-2">
- %= form_for move => (id => 'move-mail') => (class => 'pure-form') => begin
- <fieldset>
- %= label_for 'select-folder' => l('move to')
- %= select_field folder => [grep {$_ ne $folder} @$mail_folders] => (id => 'select-folder')
- %= csrf_field
- %= submit_button l('move') => (class => 'pure-button')
- </fieldset>
- % end
+ % if (grep {$_ ne $folder} @$mail_folders) {
+ %= form_for move => (id => 'move-mail') => (class => 'pure-form') => begin
+ <fieldset>
+ %= label_for 'select-folder' => l('move to')
+ %= select_field folder => [map { $_ ? $_ : l 'Home' } grep {$_ ne $folder} @$mail_folders] => (id => 'select-folder')
+ %= csrf_field
+ %= submit_button l('move') => (class => 'pure-button')
+ </fieldset>
+ % end
+ % }
</div>
</div>
diff --git a/templates/headers/_display_folders.html.ep b/templates/headers/_display_folders.html.ep
index 856844d..7f1612f 100644
--- a/templates/headers/_display_folders.html.ep
+++ b/templates/headers/_display_folders.html.ep
@@ -8,10 +8,10 @@
</strong>
<ul class="pure-menu-list">
-% for my $v (grep {$_ ne $folder} @$mail_folders) {
+% for (grep {$_ ne $folder} @$mail_folders) {
<li class="pure-menu-item">
- %= link_to '' => {folder => $v} => (class => 'bright') => begin
- %= l($v || '_mailbox_root')
+ %= link_to '' => {folder => $_} => (class => 'bright') => begin
+ %= l($_ || '_mailbox_root')
% end
</li>
% }
@@ -21,7 +21,7 @@
</div>
<p class="pure-u-1-1 pure-u-md-1-2">
- <%= l('[_1] of [_2] messages', $last_item - $first_item + 1, $total_items) %>\
+ <%= l('[_1] of [_2] messages', $this_page->[1] - $this_page->[0], $total_items) %>\
<%= l(', [_1] new', $total_new_mails) if $total_new_mails > 0 =%>
<%= l(' - mailbox size: [_1]', print_sizes10 $total_size) if $total_size %>
</p>
diff --git a/templates/headers/_display_headers.html.ep b/templates/headers/_display_headers.html.ep
index 42f927c..4dd36a1 100644
--- a/templates/headers/_display_headers.html.ep
+++ b/templates/headers/_display_headers.html.ep
@@ -66,9 +66,9 @@
% foreach my $msgnum ($first_item .. $last_item) {
% my $msg = $msgs->[$msgnum - $first_item];
- %= tag tr => (class => $msg->{new} ? 'new-mail' : '') => (id => $msg->{mid}) => begin
+ %= tag tr => (class => $msg->{unread} ? 'new-mail' : '') => (id => $msg->{message_handle}) => begin
<td class="hide-small">
- %= $msgnum
+ %= $msgnum + 1
</td>
<td>
@@ -76,7 +76,7 @@
<!--
<div class="pure-u-1-4">
- %# ucfirst($msg->{is_multipart} ? l('yes') : l('no'));
+ %# ucfirst($msg->{head}{mime}{content_maintype} eq 'multipart' ? l('yes') : l('no'));
</div>
-->
@@ -86,22 +86,23 @@
</div>
<div class="pure-u-16-24 pure-u-md-6-24">
- %= $msg->{head}{from}[0]{name} || $msg->{head}{from}[0]{email};
+ <%= $msg->{head}{sender}[0]{display_name} || $msg->{head}{sender}[0]{address} ||
+ $msg->{head}{from}[0]{display_name} || $msg->{head}{from}[0]{address}; %>
</div>
<div class="pure-u-20-24 pure-u-md-12-24">
- %= link_to $msg->{head}{subject} || '_' => read => {id => $msg->{mid}}
+ %= link_to $msg->{head}{subject} || '_' => read => {id => $msg->{message_handle}}
</div>
<div class="pure-u-4-24 pure-u-md-2-24">
- %= print_sizes10 $msg->{size};
+ %= print_sizes10 $msg->{byte_size};
</div>
</div>
</td>
<td>
- %= check_box mail => $msg->{mid} => (form => 'move-mail')
+ %= check_box mail => $msg->{message_handle} => (form => 'move-mail')
</td>
% end
diff --git a/templates/headers/_pagination1.html.ep b/templates/headers/_pagination1.html.ep
index 9b6121a..0e000f9 100644
--- a/templates/headers/_pagination1.html.ep
+++ b/templates/headers/_pagination1.html.ep
@@ -1,7 +1,7 @@
<div>
- <a href="<%= url_with->query({start => $prev_page->[0]-1}) %>"><img src="/left.gif" alt="←"></a>
- <a href="<%= url_with->query({start => $first_page->[0]-1}) %>"><img src="/first.gif" alt="↞"></a>
- [<%= l('page [_1] of [_2]', $current_page, $total_pages) %>]
- <a href="<%= url_with->query({start => $last_page->[0]-1}) %>"><img src="/last.gif" alt="↠"></a>
- <a href="<%= url_with->query({start => $next_page->[0]-1}) %>"><img src="/right.gif" alt="→"></a>
+ <a href="<%= url_with->query({start => $prev_page->[0]}) %>"> ← </a>
+ <a href="<%= url_with->query({start => $first_page->[0]}) %>"> ↞ </a>
+ [<%= l('page [_1] of [_2]', $current_page+1, $total_pages) %>]
+ <a href="<%= url_with->query({start => $last_page->[0]}) %>"> ↠ </a>
+ <a href="<%= url_with->query({start => $next_page->[0]}) %>"> → </a>
</div>
diff --git a/templates/headers/_pagination2.html.ep b/templates/headers/_pagination2.html.ep
index 236e9bb..63e8f63 100644
--- a/templates/headers/_pagination2.html.ep
+++ b/templates/headers/_pagination2.html.ep
@@ -1,10 +1,10 @@
<div>
%= form_for '' => (class => 'pure-form') => begin
- <a href="<%= url_with->query({start => $first_page->[0]-1}) %>"><img src="/first.gif" alt="<%= l('first') . ' ' . l 'page' %>"></a>
- <a href="<%= url_with->query({start => $prev_page->[0]-1}) %>"><img src="/left.gif" alt="<%= l('previous') . ' ' . l 'page' %>"></a>
+ <a href="<%= url_with->query({start => $first_page->[0]}) %>"><img src="/first.gif" alt="<%= l('first') . ' ' . l 'page' %>"></a>
+ <a href="<%= url_with->query({start => $prev_page->[0]}) %>"><img src="/left.gif" alt="<%= l('previous') . ' ' . l 'page' %>"></a>
[
%= label_for custompage => ucfirst l 'page'
- %= number_field start => (id => 'custompage') => (size => 3) => (placeholder => $current_page)
+ %= number_field start => (id => 'custompage') => (size => 3) => (placeholder => $current_page+1)
%= l 'of'
%= $total_pages
]
@@ -16,7 +16,7 @@
% }
% }
- <a href="<%= url_with->query({start => $next_page->[0]-1}) %>"><img src="/right.gif" alt="<%= l('next') . ' ' . l 'page' %>"></a>
- <a href="<%= url_with->query({start => $last_page->[0]-1}) %>"><img src="/last.gif" alt="<%= l('last') . ' ' . l('page') %>"></a>
+ <a href="<%= url_with->query({start => $next_page->[0]}) %>"><img src="/right.gif" alt="<%= l('next') . ' ' . l 'page' %>"></a>
+ <a href="<%= url_with->query({start => $last_page->[0]}) %>"><img src="/last.gif" alt="<%= l('last') . ' ' . l('page') %>"></a>
% end
</div>
diff --git a/templates/webmail/readmail.html.ep b/templates/webmail/readmail.html.ep
index 5bad9f3..b5b48a1 100644
--- a/templates/webmail/readmail.html.ep
+++ b/templates/webmail/readmail.html.ep
@@ -2,10 +2,14 @@
% my $mail_fmt = begin
% my ($category, $value) = @_;
- <dt> <%= ucfirst l $category %> </dt>
- <dd>
- %= ref $value ? join(' ' . l('and') . ' ', map {"$_->{name} <$_->{address}>"} @$value) : $value
- </dd>
+ % if (ref $value eq 'ARRAY' && $value->@*) {
+ <dt> <%= uc l $category %> </dt>
+ % for ($value->@*) {
+ <dd>
+ %= $_->{name} ? qq("$_->{name}" <$_->{address}>) : "$_->{address}"
+ </dd>
+ % }
+ % }
% end
<div class="jwm-base">
@@ -16,42 +20,43 @@
<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}) if !ref $msg->{head}{cc} || @{ $msg->{head}{cc} }
- %= $mail_fmt->('bcc', $msg->{head}{bcc}) if !ref $msg->{head}{bcc} || @{ $msg->{head}{bcc} }
+ %= $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 'size' %> </dt>
- <dd> <%= print_sizes10 $msg->{size} %> </dd>
-
+ % my $content_type = $msg->{head}{mime}{content_maintype} . '/' . $msg->{head}{mime}{content_subtype};
<dt> <%= uc l 'content-type' %> </dt>
- <dd> <%= $msg->{head}{content_type} %> </dd>
+ <dd> <%= $content_type %> </dd>
</dl>
% my $body = $msg->{body};
-% if ($msg->{head}{content_type} eq 'multipart/alternative') {
-% for (reverse @$body) {
+% if ($content_type eq 'multipart/alternative') {
+% for (reverse @{$body->{parts}}) {
<div class=jwm-mail-body>
-% my $x = mime_render($_->{head}{content_type}, $_->{body});
+% my $x = mime_render($_->{head}{content_maintype}.'/'.$_->{head}{content_subtype}, $_->{body});
%== $x;
</div>
% last if $x;
% }
% }
-% elsif (ref $body eq 'HASH') {
-% for (%$body) {
+% elsif ($msg->{head}{mime}{content_maintype} eq 'multipart') {
+% for (@{$body->{parts}}) {
<div class=jwm-mail-body>
- %== mime_render($_->{head}{content_type}, $_->{body});
+ %== mime_render($_->{head}{content_maintype}.'/'.$_->{head}{content_subtype}, $_->{body});
</div>
% }
% }
+% elsif ($msg->{head}{mime}{content_maintype} eq 'message') {
+% die "not implemented"
+% }
% else {
<div class=jwm-mail-body>
- %== mime_render($msg->{head}{content_type}, $body);
+ %== mime_render($content_type, $body);
</div>
% }