summaryrefslogtreecommitdiff
path: root/lib/JWebmail/Plugin
diff options
context:
space:
mode:
Diffstat (limited to 'lib/JWebmail/Plugin')
-rw-r--r--lib/JWebmail/Plugin/Helper.pm448
-rw-r--r--lib/JWebmail/Plugin/I18N.pm212
-rw-r--r--lib/JWebmail/Plugin/I18N2.pm185
-rw-r--r--lib/JWebmail/Plugin/INIConfig.pm136
-rw-r--r--lib/JWebmail/Plugin/ServerSideSessionData.pm147
5 files changed, 1128 insertions, 0 deletions
diff --git a/lib/JWebmail/Plugin/Helper.pm b/lib/JWebmail/Plugin/Helper.pm
new file mode 100644
index 0000000..5e557d1
--- /dev/null
+++ b/lib/JWebmail/Plugin/Helper.pm
@@ -0,0 +1,448 @@
+package JWebmail::Plugin::Helper;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use POSIX qw(floor round log ceil);
+use MIME::Base64;
+use Encode;
+use Mojo::Util 'xml_escape';
+use List::Util qw(min max);
+
+use constant TRUE_RANDOM => eval { require Crypt::Random; Crypt::Random->import('makerandom_octet'); 1 };
+use constant HMAC => eval { require Digest::HMAC_MD5; Digest::HMAC_MD5->import('hmac_md5'); 1 };
+
+### filter and checks for mojo validator
+
+sub mail_line {
+ my ($v, $name, $value, @args) = @_;
+
+ my $mail_addr = qr/\w+\@\w+\.\w+/;
+ # my $unescaped_quote = qr/"(*nlb:\\)/; # greater perl version required
+ my $unescaped_quote = qr/"(?<!:\\)/;
+
+ return $value !~ /^(
+ (
+ (
+ (
+ $unescaped_quote.*?$unescaped_quote
+ ) | (
+ [\w\s]*
+ )
+ )
+ \s*<$mail_addr>
+ ) | (
+ $mail_addr
+ ))$
+ /xno;
+}
+
+
+sub filter_empty_upload {
+ my ($v, $name, $value) = @_;
+
+ return $value->filename ? $value : undef;
+}
+
+### template formatting functions
+
+sub print_sizes10 {
+ my $var = shift;
+ if ($var == 0) { return '0 Byte'; }
+
+ my $i = floor(((log($var)/log(10))+1e-5) / 3);
+ my $expo = $i * 3;
+
+ my @PREFIX;
+ $PREFIX[0] = 'Byte';
+ $PREFIX[1] = 'kByte';
+ $PREFIX[2] = 'MByte';
+ $PREFIX[3] = 'GByte';
+ $PREFIX[4] = 'TByte';
+ $PREFIX[5] = 'PByte';
+
+ return sprintf('%.0f %s', $var / (10**$expo), $PREFIX[$i]);
+}
+
+
+sub print_sizes2 {
+ my $var = shift;
+ if ($var == 0) { return '0 Byte'; }
+
+ my $i = floor(((log($var)/log(2))+1e-5) / 10);
+ my $expo = $i * 10;
+ my %PREFIX = (
+ 0 => 'Byte',
+ 1 => 'KiByte',
+ 2 => 'MiByte',
+ 3 => 'GiByte',
+ 4 => 'TiByte',
+ 5 => 'PiByte',
+ );
+ my $pref = $PREFIX{$i};
+ return round($var / (2**$expo)) . " $pref";
+}
+
+### 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('rawid', id => $c_->stash('id'))->query(body => 'html') . '" class=html-mail />';
+};
+
+
+our %MIME_Render_Subs = (
+ 'text/plain' => $render_text_plain,
+ 'text/html' => $render_text_html,
+);
+
+
+sub mime_render {
+ my ($c, $enc, $cont) = @_;
+
+ my $renderer = $MIME_Render_Subs{$enc};
+ return '' unless defined $renderer;
+ return $renderer->($c, $cont);
+};
+
+### session password handling
+
+use constant { S_PASSWD => 'pw', S_OTP_S3D_PW => 'otp_s3d_pw' };
+
+sub _rand_data {
+ my $len = shift;
+
+ return makerandom_octet(Length => $len, Strength => 0);
+}
+
+sub _pseudo_rand_data {
+ my $len = shift;
+
+ my $res = '';
+ for (0..$len-1) {
+ vec($res, $_, 8) = int rand 256;
+ }
+
+ return $res;
+}
+
+sub session_passwd {
+ my ($c, $passwd) = @_;
+
+ warn_cram($c);
+ warn_crypt($c);
+
+ if (defined $passwd) { # set
+ if ( HMAC && lc($c->config->{'session'}{secure} || 'none') eq 'cram' ) {
+ $c->session(S_PASSWD() => $passwd ? encode_base64(hmac_md5($passwd, $c->app->secrets->[0]), '') : '');
+ }
+ elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') {
+ unless ($passwd) {
+ $c->s3d(S_PASSWD, '');
+ delete $c->session->{S_OTP_S3D_PW()};
+ return;
+ }
+ die "'$passwd' contains invalid character \\n" if $passwd =~ /\n/;
+ if (length $passwd < 20) {
+ $passwd .= "\n" . " " x (20 - length($passwd) - 1);
+ }
+ my $rand_bytes = TRUE_RANDOM ? _rand_data(length $passwd) : _pseudo_rand_data(length $passwd);
+ $c->s3d(S_PASSWD, encode_base64(encode('UTF-8', $passwd) ^ $rand_bytes, ''));
+ $c->session(S_OTP_S3D_PW, encode_base64($rand_bytes, ''));
+ }
+ else {
+ $c->session(S_PASSWD() => $passwd);
+ }
+ }
+ else { # get
+ if ( HMAC && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) {
+ return ($c->app->secrets->[0], $c->session(S_PASSWD));
+ }
+ elsif (lc($c->config->{'session'}->{secure} || 'none') eq 's3d') {
+ my $pw = decode_base64($c->s3d(S_PASSWD) || '');
+ my $otp = decode_base64($c->session(S_OTP_S3D_PW) || '');
+ my ($res) = split "\n", decode('UTF-8', $pw ^ $otp), 2;
+ return $res;
+ }
+ else {
+ return $c->session(S_PASSWD);
+ }
+ }
+}
+
+sub warn_cram {
+ my $c = shift;
+
+ state $once = 0;
+
+ if ( !HMAC && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 'cram' ) {
+ $c->log->warn("cram requires Digest::HMAC_MD5. Falling back to 'none'.");
+ }
+
+ $once = 1;
+}
+
+sub warn_crypt {
+ my $c = shift;
+
+ state $once = 0;
+
+ if ( !TRUE_RANDOM && !$once && lc($c->config->{'session'}->{secure} || 'none') eq 's3d' ) {
+ $c->log->warn("Falling back to pseudo random generation. Please install Crypt::Random");
+ }
+
+ $once = 1;
+}
+
+### pagination
+
+sub _clamp {
+ my ($x, $y, $z) = @_;
+
+ die '!($x <= $z)' unless $x <= $z;
+
+ if ($x <= $y && $y <= $z) {
+ return $y;
+ }
+
+ return $x if ($y < $x);
+ return $z if ($z < $y);
+}
+
+sub _paginate {
+ my %args = @_;
+
+ my $first_item = $args{first_item};
+ my $page_size = $args{page_size} || 1;
+ 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);
+
+ my $page = sub {
+ 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)]
+ };
+
+ return (
+ first_item => $first_item1,
+ last_item => _clamp($first_item1, $first_item + $page_size, $total_items),
+ total_items => $total_items,
+ page_size => $page_size,
+
+ total_pages => $total_pages,
+ current_page => $current_page + 1,
+
+ first_page => $page->(0),
+ prev_page => $page->($current_page-1),
+ next_page => $page->($current_page+1),
+ last_page => $page->($total_pages-1),
+ );
+}
+
+sub paginate {
+ my $c = shift;
+ my $count = shift;
+
+ my $v = $c->validation;
+ my $start = $v->optional('start')->num(0, undef)->param // 0;
+ 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));
+
+ $c->stash(_paginate(first_item => $start, page_size => $psize, total_items => $count));
+
+ return $start, $end;
+}
+
+### registering
+
+sub register {
+ my ($self, $app, $conf) = @_;
+
+ if (ref $conf->{import} eq 'ARRAY' and my @import = @{ $conf->{import} }) {
+ no warnings 'experimental::smartmatch';
+
+ # selective import
+ $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) })
+ if 'print_sizes10' ~~ @import;
+ $app->helper(print_sizes2 => sub { shift; print_sizes2(@_) })
+ if 'print_sizes2' ~~ @import;
+ $app->helper(mime_render => \&mime_render)
+ if 'mime_render' ~~ @import;
+ $app->helper(session_passwd => \&session_passwd)
+ if 'session_passwd' ~~ @import;
+ $app->helper(paginate => \&paginate)
+ if 'paginate' ~~ @import;
+ $app->validator->add_check(mail_line => \&mail_line)
+ if 'mail_line' ~~ @import;
+ $app->validator->add_filter(non_empty_ul => \&filter_empty_upload)
+ if 'non_empty_ul' ~~ @import;
+ }
+ elsif (!$conf->{import}) { # default imports
+ $app->helper(print_sizes10 => sub { shift; print_sizes10(@_) });
+ $app->helper(mime_render => \&mime_render);
+ $app->helper(session_passwd => \&session_passwd);
+ $app->helper(paginate => \&paginate);
+
+ $app->validator->add_check(mail_line => \&mail_line);
+
+ $app->validator->add_filter(non_empty_ul => \&filter_empty_upload);
+ }
+}
+
+
+1
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Helper - Functions used as helpers in controller and templates and additional validator checks and filters
+
+=head1 SYNOPSIS
+
+ use Mojo::Base 'Mojolicious';
+
+ use JWebmail::Plugin::Helper;
+
+ sub startup($self) {
+ $self->helper(mime_render => \&JWebmail::Plugin::Helper::mime_render);
+ }
+
+ # or
+
+ $app->plugin('Helper');
+
+=head1 DESCRIPTION
+
+L<JWebmail::Helper> provides useful helper functions and validator cheks and filter for
+L<JWebmail::Controller::All> and various templates.
+
+=head1 FUNCTIONS
+
+=head2 mail_line
+
+A check for validator used in mail headers for fields containing email addresses.
+
+ $app->validator->add_check(mail_line => \&JWebmail::Plugin::Helper::mail_line);
+
+ my $v = $c->validation;
+ $v->required('to', 'not_empty')->check('mail_line');
+
+=head2 filter_empty_upload
+
+A filter for validator used to filter out empty uploads.
+
+ $app->validator->add_filter(non_empty_ul => \&JWebmail::Plugin::Helper::filter_empty_upload);
+
+ my $v = $c->validation;
+ $v->required('file_upload', 'non_empty_ul');
+
+=head2 print_sizes10
+
+A helper for templates used to format byte sizes.
+
+ $app->helper(print_sizes10 => sub { shift; JWebmail::Plugin::Helper::print_sizes10(@_) });
+
+ %= print_sizes10 12345 # => 12 kB
+
+=head2 print_sizes2
+
+A helper for templates used to format byte sizes.
+
+ %= print_sizes10 12345 # => 12 KiB
+
+This is not registered by default.
+
+=head2 paginate
+
+A helper for calculationg page bounds.
+
+Takes the total number of items as argument.
+
+Reads in 'start' and 'page_size' query arguments.
+start is 0 based.
+
+Returns the calculated start and end points as 0 based inclusive range.
+
+Sets the stash values (all 1 based inclusive):
+
+ first_item
+ last_item
+ total_items
+ page_size
+ total_pages
+ current_page
+ first_page
+ prev_page
+ 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 behaivour can be altered by
+setting the config variable C<< session => {secure => 's3d'} >>.
+
+ $app->helper(session_passwd => \&JWebmail::Plugin::Helper::session_passwd);
+
+ $c->session_passwd('s3cret');
+
+Currently the following modes are supported:
+
+=over 6
+
+=item none
+
+password is plainly stored in session cookie
+
+=item cram
+
+challenge response authentication mechanism uses the C<< $app->secret->[0] >> as nonce.
+This is optional if Digest::HMAC_MD5 is installed.
+
+=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.
+
+=back
+
+=head1 DEPENDENCIES
+
+Mojolicious, Crypt::Random and optianally Digest::HMAC_MD5.
+
+=head1 SEE ALSO
+
+L<JWebmail>, L<JWebmail::Controller::All>, L<Mojolicious>, L<Mojolicious::Controller>
+
+=head1 NOTICE
+
+This package is part of JWebmail.
+
+=cut \ No newline at end of file
diff --git a/lib/JWebmail/Plugin/I18N.pm b/lib/JWebmail/Plugin/I18N.pm
new file mode 100644
index 0000000..dc10fdd
--- /dev/null
+++ b/lib/JWebmail/Plugin/I18N.pm
@@ -0,0 +1,212 @@
+package JWebmail::Plugin::I18N;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use Mojolicious::Controller;
+use Mojo::File;
+use Mojo::Util 'monkey_patch';
+
+
+has '_language_loaded' => sub { {} };
+
+
+sub register {
+ my ($self, $app, $conf) = @_;
+
+ my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']');
+
+ # config
+ # 1. what languages
+ # 2. where are the files
+ # 3. fallback language
+ #
+ # look for languages automatically
+ my $defaultLang = $conf->{default_language} || 'en';
+ my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs
+ ? $conf->{directory}
+ : $app->home->child($conf->{directory} || 'lang');
+ my @languages = keys %{$conf->{languages} // {}};
+
+ unless (@languages) {
+ @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang");
+ }
+
+ $app->defaults(lang => $defaultLang);
+ $app->defaults(languages => [@languages]);
+
+ # load languages
+ my $TXT;
+ for my $l (@languages) {
+ $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log);
+ }
+
+ {
+ local $" = ',';
+ $i18n_log->debug("loaded languages (@languages)");
+ }
+
+ $self->_language_loaded( { map { $_ => 1 } @languages } );
+
+ # add translator as helper
+ my $i18n = sub {
+ my ($lang, $word) = @_;
+ $TXT->{$lang}{$word} || scalar(
+ local $" = ' ',
+ $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (),
+ '',
+ )
+ };
+ $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } );
+
+ # rewrite url
+ $app->hook(before_dispatch => sub { $self->read_language_hook(@_) });
+
+ # patch url_for
+ my $mojo_url_for = Mojolicious::Controller->can('url_for');
+ my $i18n_url_for = sub {
+ my $c = shift;
+ my $url = $mojo_url_for->($c, @_);
+
+ my $args = (ref $_[0] eq 'HASH' and $_[0]) || (ref $_[1] eq 'HASH' and $_[1]) || do { my %x = @_[(@_ % 2) .. $#_]; \%x };
+ my $lang = $args->{lang} // $c->stash->{lang};
+
+ if ( $lang && (ref $_[0] eq 'HASH' || !ref $_[0] && ($_[0]//'') !~ m![:@/.]!) ) {
+ unshift @{ $url->path->parts }, $lang
+ if ($url->path->parts->[0] // '') ne $lang;
+ $url = $url->to_abs(Mojo::URL->new('/'));
+ }
+
+ return $url;
+ };
+ monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for;
+
+ 0
+}
+
+
+sub read_language_hook {
+ my $self = shift;
+ my $c = shift;
+
+ # URL detection
+ if (my $path = $c->req->url->path) {
+
+ my $part = $path->parts->[0];
+
+ if ( $part && $self->_language_loaded->{$part} ) {
+ # Ignore static files
+ return if $c->res->code;
+
+ $c->app->log->debug('[' . __PACKAGE__ . "] Found language $part in URL $path");
+
+ # Save lang in stash
+ $c->stash(lang => $part);
+
+ if ( @{ $path->parts } == 1 && !$path->trailing_slash ) {
+ return $c->redirect_to($c->req->url->path->trailing_slash(1)); # default controller adds language back
+ }
+
+ # Clean path
+ shift @{$path->parts};
+ $path->trailing_slash(0);
+ }
+ }
+}
+
+
+sub _loadi18n {
+
+ my $langsubdir = shift;
+ my $lang = shift;
+ my $log = shift;
+
+ my $langFile = "$langsubdir/$lang.lang";
+ my $TXT;
+
+ if ( -f $langFile ) {
+ $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'};
+ if ($@ || !defined $TXT) {
+ $log->error("error reading file $langFile: $@");
+ }
+ }
+ else {
+ $log->warn("language file $langFile does not exist!");
+ }
+ return $TXT;
+}
+
+
+1
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+JWebmail::Plugin::I18N - Custom Made I18N Support Inspired by Mojolicious::Plugin::I18N
+
+=head1 SYNOPSIS
+
+ $app->plugin('I18N', {
+ languages => [qw(en de es)],
+ default_language => 'en',
+ directory => '/path/to/language/files/',
+ })
+
+ # in your controller
+ $c->l('hello')
+
+ # in your templates
+ <%= l 'hello' %>
+
+ @@ de.lang
+ login = anmelden
+ userid = nuzerkennung
+ passwd = passwort
+ failed = fehlgeschlagen
+ about = über
+
+ example.com/de/myroute # $c->stash('lang') eq 'de'
+ example.com/myroute # $c->stash('lang') eq $defaultLanguage
+
+ # on example.com/de/myroute
+ url_for('my_other_route') #=> example.com/de/my_other_route
+
+ url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route
+
+=head1 DESCRIPTION
+
+L<JWebmail::Plugin::I18N> provides I18N support.
+
+The language will be taken from the first path segment of the url.
+Be carefult with colliding routes.
+
+Mojolicious::Controller::url_for is patched so that the current language will be kept for
+router named urls.
+
+=head1 OPTIONS
+
+=head2 default_language
+
+The default language when no other information is provided.
+
+=head2 directory
+
+Directory to look for language files.
+
+=head2 languages
+
+List of allowed languages.
+Files of the pattern "$lang.lang" will be looked for.
+
+=head1 HELPERS
+
+=head2 l
+
+This is used for your translations.
+
+ $c->l('hello')
+ $app->helper('hello')->()
+
+=cut \ No newline at end of file
diff --git a/lib/JWebmail/Plugin/I18N2.pm b/lib/JWebmail/Plugin/I18N2.pm
new file mode 100644
index 0000000..53813de
--- /dev/null
+++ b/lib/JWebmail/Plugin/I18N2.pm
@@ -0,0 +1,185 @@
+package JWebmail::Plugin::I18N2;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use Mojolicious::Controller;
+use Mojo::File;
+use Mojo::Util 'monkey_patch';
+
+
+has '_language_loaded' => sub { {} };
+
+
+sub register {
+ my ($self, $app, $conf) = @_;
+
+ my $i18n_log = $app->log->context('[' . __PACKAGE__ . ']');
+
+ # config
+ # 1. what languages
+ # 2. where are the files
+ # 3. fallback language
+ #
+ # look for languages automatically
+ my $defaultLang = $conf->{default_language} || 'en';
+ my $fileLocation = $conf->{directory} && Mojo::File->new($conf->{directory})->is_abs
+ ? $conf->{directory}
+ : $app->home->child($conf->{directory} || 'lang');
+ my @languages = keys %{$conf->{languages} // {}};
+
+ unless (@languages) {
+ @languages = map { $_ =~ s|^.*/(..)\.lang$|$1|r } glob("$fileLocation/*.lang");
+ }
+
+ $app->defaults(languages => [@languages]);
+
+ # load languages
+ my $TXT;
+ for my $l (@languages) {
+ $TXT->{$l} = _loadi18n($fileLocation, $l, $i18n_log);
+ }
+
+ {
+ local $" = ',';
+ $i18n_log->debug("loaded languages (@languages)");
+ }
+
+ $self->_language_loaded( { map { $_ => 1 } @languages } );
+
+ # add translator as helper
+ my $i18n = sub {
+ my ($lang, $word) = @_;
+ $TXT->{$lang}{$word} || scalar(
+ local $" = ' ',
+ $lang && $word ? $app->log->debug('[' . __PACKAGE__ . "] missing translation for $lang:$word @{[ caller(2) ]}[0..2]") : (),
+ '',
+ )
+ };
+ $app->helper( l => sub { my $c = shift; $i18n->($c->stash->{lang}, shift) } );
+
+ $app->hook(before_dispatch => sub {
+ my $c = shift;
+ unshift @{ $c->req->url->path->parts }, ''
+ unless $self->_language_loaded->{$c->req->url->path->parts->[0] || ''};
+ });
+
+ # patch url_for
+ my $mojo_url_for = Mojolicious::Controller->can('url_for');
+ my $i18n_url_for = sub {
+ my $c = shift;
+ if (ref $_[0] eq 'HASH') {
+ $_[0]->{lang} ||= $c->stash('lang');
+ }
+ elsif (ref $_[1] eq 'HASH') {
+ $_[1]->{lang} ||= $c->stash('lang');
+ }
+ elsif (@_) {
+ push @_, lang => $c->stash('lang');
+ }
+ else {
+ @_ = {lang => $c->stash('lang')};
+ }
+ return $mojo_url_for->($c, @_);
+ };
+ monkey_patch 'Mojolicious::Controller', url_for => $i18n_url_for;
+
+ return $app->routes->any('/:lang' => {lang => 'en'});
+}
+
+
+sub _loadi18n {
+
+ my $langsubdir = shift;
+ my $lang = shift;
+ my $log = shift;
+
+ my $langFile = "$langsubdir/$lang.lang";
+ my $TXT;
+
+ if ( -f $langFile ) {
+ $TXT = Config::Tiny->read($langFile, 'utf8')->{'_'};
+ if ($@ || !defined $TXT) {
+ $log->error("error reading file $langFile: $@");
+ }
+ }
+ else {
+ $log->warn("language file $langFile does not exist!");
+ }
+ return $TXT;
+}
+
+
+1
+
+__END__
+
+=encoding utf8
+
+=head1 NAME
+
+JWebmail::Plugin::I18N2 - Custom Made I18N Support an alternative to JWebmail::Plugin::I18N
+
+=head1 SYNOPSIS
+
+ $app->plugin('I18N2', {
+ languages => [qw(en de es)],
+ default_language => 'en',
+ directory => '/path/to/language/files/',
+ })
+
+ # in your controller
+ $c->l('hello')
+
+ # in your templates
+ <%= l 'hello' %>
+
+ @@ de.lang
+ login = anmelden
+ userid = nuzerkennung
+ passwd = passwort
+ failed = fehlgeschlagen
+ about = über
+
+ example.com/de/myroute # $c->stash('lang') eq 'de'
+ example.com/myroute # $c->stash('lang') eq $defaultLanguage
+
+ # on example.com/de/myroute
+ url_for('my_other_route') #=> example.com/de/my_other_route
+
+ url_for('my_other_route', lang => 'es') #=> example.com/es/my_other_route
+
+=head1 DESCRIPTION
+
+L<JWebmail::Plugin::I18N2> provides I18N support.
+
+The language will be taken from the first path segment of the url.
+Be carefult with colliding routes.
+
+Mojolicious::Controller::url_for is patched so that the current language will be kept for
+router named urls.
+
+=head1 OPTIONS
+
+=head2 default_language
+
+The default language when no other information is provided.
+
+=head2 directory
+
+Directory to look for language files.
+
+=head2 languages
+
+List of allowed languages.
+Files of the pattern "$lang.lang" will be looked for.
+
+=head1 HELPERS
+
+=head2 l
+
+This is used for your translations.
+
+ $c->l('hello')
+ $app->helper('hello')->()
+
+=cut \ No newline at end of file
diff --git a/lib/JWebmail/Plugin/INIConfig.pm b/lib/JWebmail/Plugin/INIConfig.pm
new file mode 100644
index 0000000..fe0fb1a
--- /dev/null
+++ b/lib/JWebmail/Plugin/INIConfig.pm
@@ -0,0 +1,136 @@
+package JWebmail::Plugin::INIConfig;
+use Mojo::Base 'Mojolicious::Plugin::Config';
+
+use List::Util 'all';
+
+use Config::Tiny;
+
+
+sub parse {
+ my ($self, $content, $file, $conf, $app) = @_;
+
+ my $ct = Config::Tiny->new;
+ my $config = $ct->read_string($content, 'utf8');
+ die qq{Can't parse config "$file": } . $ct->errstr unless defined $config;
+
+ $config = _process_config($config) unless $conf->{flat};
+
+ return $config;
+}
+
+
+sub _process_config {
+ my $val_prev = shift;
+ my %val = %$val_prev;
+
+ # arrayify section with number keys
+ for my $key (keys %val) {
+ if (keys %{$val{$key}} && all { $_ =~ /\d+/} keys %{$val{$key}}) {
+ my $tmp = $val{$key};
+ $val{$key} = [];
+
+ for (keys %$tmp) {
+ $val{$key}[$_] = $tmp->{$_};
+ }
+ }
+ }
+
+ # merge top section
+ my $top_section = $val{'_'};
+ delete $val{'_'};
+ for (keys %$top_section) {
+ $val{$_} = $top_section->{$_} unless $val{$_};
+ }
+
+ # make implicit nesting explicit
+ for my $key (grep { $_ =~ /^\w+(::\w+)+$/} keys %val) {
+
+ my @sections = split m/::/, $key;
+ my $x = \%val;
+ my $y;
+ for (@sections) {
+ $x->{$_} = {} unless ref $x->{$_};# eq 'HASH';
+ $y = $x;
+ $x = $x->{$_};
+ }
+ # merge
+ if (ref $val{$key} eq 'ARRAY') {
+ $y->{$sections[-1]} = [];
+ $x = $y->{$sections[-1]};
+ for ( keys @{ $val{$key} } ) {
+ $x->[$_] = $val{$key}[$_];
+ }
+ }
+ else {
+ for ( keys %{ $val{$key} } ) {
+ $x->{$_} = $val{$key}{$_};
+ }
+ }
+ delete $val{$key};
+ }
+
+ return \%val
+}
+
+
+1
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+INIConfig - Reads in ini config files.
+
+=head1 SYNOPSIS
+
+ $app->plugin('INIConfig');
+
+ @@ my_app.conf
+
+ # global section
+ key = val ; line comment
+ [section]
+ other_key = other_val
+ [other::section]
+ 0 = key1
+ 1 = key2
+ 2 = key3
+
+=head1 DESCRIPTION
+
+INI configuration is simple with limited nesting and propper comments.
+For more precise specification on the syntax see the Config::Tiny documentation
+on metacpan.
+
+=head1 OPTIONS
+
+=head2 default
+
+Sets default configuration values.
+
+=head2 ext
+
+Sets file extension defaults to '.conf'.
+
+=head2 file
+
+Sets file name default '$app->moniker'.
+
+=head2 flat
+
+Keep configuration to exactly two nesting levels for all
+and disable auto array conversion.
+
+=head1 METHODS
+
+=head2 parse
+
+overrides the parse method of Mojolicious::Plugin::Config
+
+=head1 DEPENDENCIES
+
+Config::Tiny
+
+=cut \ No newline at end of file
diff --git a/lib/JWebmail/Plugin/ServerSideSessionData.pm b/lib/JWebmail/Plugin/ServerSideSessionData.pm
new file mode 100644
index 0000000..9890358
--- /dev/null
+++ b/lib/JWebmail/Plugin/ServerSideSessionData.pm
@@ -0,0 +1,147 @@
+package JWebmail::Plugin::ServerSideSessionData;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use Mojo::JSON qw(decode_json encode_json);
+use Mojo::File;
+
+use constant {
+ S_KEY => 's3d.key',
+};
+
+
+has '_session_directory';
+sub session_directory { my $self = shift; @_ ? $self->_session_directory(Mojo::File->new(@_)) : $self->_session_directory }
+
+has 'expiration';
+has 'cleanup_interval';
+
+has '_cleanup';
+sub cleanup {
+ my $self = shift;
+ if (@_) {
+ return $self->_cleanup(@_);
+ }
+ else {
+ if ($self->_cleanup < time) {
+ return 0;
+ }
+ else {
+ $self->_cleanup(time + $self->cleanup_interval);
+ return 1;
+ }
+ }
+}
+
+
+sub s3d {
+ my $self = shift;
+ my $c = shift;
+
+ # cleanup old sessions
+ if ($self->cleanup) {
+ my $t = time;
+ for ($self->session_directory->list->each) {
+ if ( $_->stat->mtime + $self->expiration < $t ) {
+ $_->remove;
+ }
+ }
+ }
+
+ my $file = $self->session_directory->child($c->session(S_KEY) || $c->req->request_id . $$);
+
+ if (-e $file) {
+ if ($file->stat->mtime + $self->expiration < time) {
+ $file->remove;
+ }
+ else {
+ $file->touch;
+ }
+ }
+ my $data = decode_json($file->slurp) if (-s $file);
+
+ my ($key, $val) = @_;
+
+ if (defined $val) { # set
+ unless (-e $file) {
+ $c->session(S_KEY, $file->basename);
+ }
+ $data = ref $data ? $data : {};
+ $data->{$key} = $val;
+
+ #$file->spurt(encode_json $data);
+ open(my $f, '>', $file) or die "$!";
+ chmod 0600, $f;
+ $f->say(encode_json $data);
+ close($f);
+ }
+ else { # get
+ return defined $key ? $data->{$key} : $data;
+ }
+};
+
+
+sub register {
+ my ($self, $app, $conf) = @_;
+
+ $self->session_directory($conf->{directory} || "/tmp/" . $app->moniker);
+ $self->expiration($conf->{expiration} || $app->sessions->default_expiration);
+ $self->cleanup_interval($conf->{cleanup_interval} || $self->expiration);
+ $self->cleanup(time + $self->cleanup_interval);
+
+ unless (-d $self->session_directory) {
+ mkdir($self->session_directory)
+ or $! ? die "failed to create directory: $!" : 1;
+ }
+
+ $app->helper( s3d => sub { $self->s3d(@_) } );
+}
+
+
+1
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+ServeSideSessionData - Stores session data on the server (alias SSSD or S3D)
+
+=head1 SYNOPSIS
+
+ $app->plugin('ServeSideSessionData');
+
+ $c->s3d(data => 'Hello, S3D');
+ $c->s3d('data');
+
+=head1 DESCRIPTION
+
+Store data temporarily on the server.
+The only protetction on the server are struct user access rights.
+
+=head1 OPTIONS
+
+=head2 directory
+
+default C<< 'tmp/' . $app->moniker >>
+
+=head2 expiration
+
+default session expiration
+
+=head2 cleanup_interval
+
+default session expiration
+
+=head1 HELPERS
+
+=head2 s3d
+
+Stores and retrieves values.
+
+ $c->s3d(data => 'Hello, S3D');
+ $c->s3d('data');
+ $c->s3d->{data};
+
+=cut \ No newline at end of file