summaryrefslogtreecommitdiff
path: root/script
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2023-02-26 21:36:27 +0100
committerJannis M. Hoffmann <jannis@fehcom.de>2023-02-26 21:36:27 +0100
commit2abf462ca10c4ac8c4f815e608cad31851e966e3 (patch)
treeed18c4a4039a5972edc0b81ed79c9d57480f8e70 /script
parentd0ffe11bd365b68d4da252b39d06d99f75d8cacb (diff)
Specified the interface for qmauth
Changes to configuration Added qmauth version written in Python Slight changes to pagination
Diffstat (limited to 'script')
-rwxr-xr-xscript/qmauth.pl273
-rwxr-xr-xscript/qmauth.py349
2 files changed, 493 insertions, 129 deletions
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)