diff options
Diffstat (limited to 'script/extract.py')
-rwxr-xr-x | script/extract.py | 414 |
1 files changed, 222 insertions, 192 deletions
diff --git a/script/extract.py b/script/extract.py index e771110..5bbbec8 100755 --- a/script/extract.py +++ b/script/extract.py @@ -7,8 +7,10 @@ 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. + +The run method is provided by a command line argument. +Additional data is read from STDIN as protobuf. +Output is delivered via STDOUT as protobuf and log information via STDERR. Exit codes:: @@ -23,7 +25,6 @@ Exit codes:: import email.parser import email.policy -import json import logging import re from argparse import ArgumentParser @@ -36,7 +37,9 @@ from os import environ, getpid, path, setuid from pathlib import Path from pwd import getpwnam from sys import exit as sysexit -from sys import stdout +from sys import stdin, stdout + +import jwebmail.model.jwebmail_pb2 as jwebmail class MyMaildir(Maildir): @@ -64,7 +67,8 @@ class MyMaildir(Maildir): def _refresh(self): super()._refresh() - for r in list(k for k in self._toc if k.startswith(".")): + rm = [r for r in self._toc if r.startswith(".")] + for r in rm: del self._toc[r] @@ -76,9 +80,12 @@ class QMAuthError(Exception): def _adr(addrs): if addrs is None: - return None + return [] return [ - {"address": addr.addr_spec, "display_name": addr.display_name} + jwebmail.MailHeader.MailAddr( + address=addr.addr_spec, + name=addr.display_name, + ) for addr in addrs.addresses ] @@ -103,9 +110,9 @@ def startup(maildir, su, user, mode): sysexit(5) def create_messages(mail_file): - if mode == count_mails: + if mode == "count": msg = MaildirMessage(None) - elif mode == list_mails: + elif mode == "list": msg = MaildirMessage( email.parser.BytesHeaderParser(policy=email.policy.default).parse( mail_file @@ -160,77 +167,107 @@ def _sort_mails(f, sort): 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(), - } + mh = jwebmail.MIMEHeader( + maintype=msg.get_content_maintype(), + subtype=msg.get_content_subtype(), + ) + if (cd := msg.get_content_disposition()) == "inline": + mh.contentdispo = ( + jwebmail.MIMEHeader.ContentDisposition.CONTENT_DISPOSITION_INLINE + ) + elif cd == "attachment": + mh.contentdispo = ( + jwebmail.MIMEHeader.ContentDisposition.CONTENT_DISPOSITION_ATTACHMENT + ) + elif cd is None: + mh.contentdispo = ( + jwebmail.MIMEHeader.ContentDisposition.CONTENT_DISPOSITION_NONE + ) + else: + assert False + + if fn := msg.get_filename(): + mh.file_name = fn + + return mh 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: + mh = jwebmail.MailHeader( + send_date=msg["date"].datetime.isoformat(), + written_from=_adr(msg["from"]), + reply_to=_adr(msg["reply-to"]), + send_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), + ) + + if s := _adr(msg["sender"]): + mh.sender = s[0] + + return mh + + +def list_mails(f, req): + r = jwebmail.ListReq() + r.ParseFromString(req) + + assert 0 <= r.start <= r.end + + if r.folder: + f = f.get_folder(r.folder) + + if r.start == r.end: return [] - kfn, reverse = _sort_mails(f, sortby) + kfn, reverse = _sort_mails(f, r.sort) msgs = list(f.items()) msgs.sort(key=kfn, reverse=reverse) - msgs = msgs[start : min(len(msgs), end)] - - return [ - { - "message_handle": mid, - "byte_size": path.getsize(f.get_filename(mid)), - "unread": "S" in msg.get_flags(), - "date_received": datetime.fromtimestamp(_get_rcv_time(mid)).isoformat(), - "head": _get_head_info(msg), - } + msgs = msgs[r.start : min(len(msgs), r.end)] + + items = [ + jwebmail.ListMailHeader( + mid=mid, + byte_size=path.getsize(f.get_filename(mid)), + unread="S" in msg.get_flags(), + rec_date=datetime.fromtimestamp(_get_rcv_time(mid)).isoformat(), + header=_get_head_info(msg), + ) for mid, msg in msgs ] + return jwebmail.ListResp(mail_heads=items).SerializeToString() -def count_mails(f, subfolder): - if subfolder: - f = f.get_folder(subfolder) +def count_mails(f, req): + r = jwebmail.StatsReq() + r.ParseFromString(req) + if r.folder: + f = f.get_folder(r.folder) - return { - "total_mails": len(f), - "byte_size": sum(path.getsize(f.get_filename(mid)) for mid in f.keys()), - "unread_mails": len([1 for m in f if "S" in m.get_flags()]), - } + resp = jwebmail.StatsResp( + mail_count=len(f), + unread_count=len([1 for m in f if "S" in m.get_flags()]), + byte_size=sum(path.getsize(f.get_filename(mid)) for mid in f.keys()), + ) + return resp.SerializeToString() def _get_body(mail): if not mail.is_multipart(): if mail.get_content_maintype() == "text": - return mail.get_content() + return jwebmail.MailBody(discrete=mail.get_content()) else: ret = mail.get_content() if ret.isascii(): - return ret.decode(encoding="ascii") + return jwebmail.MailBody(discrete=ret.decode(encoding="ascii")) elif len(ret) <= 128 * 1024: - return b64encode(ret).decode(encoding="ascii") + return jwebmail.MailBody( + discrete=b64encode(ret).decode(encoding="ascii") + ) else: raise QMAuthError( "non attachment part too large (>512kB)", size=len(ret) @@ -238,105 +275,102 @@ def _get_body(mail): if (mctype := mail.get_content_maintype()) == "message": msg = mail.get_content() - return { - "head": _get_head_info(msg), - "body": _get_body(msg), - } + return jwebmail.MailBody( + mail=jwebmail.Mail(head=_get_head_info(msg), body=_get_body(msg)) + ) elif mctype == "multipart": - ret = { - "preamble": mail.preamble, - "parts": [], - "epilogue": mail.epilogue, - } + ret = jwebmail.MailBody.Multipart( + preamble=mail.preamble, + epilogue=mail.epilogue, + ) for part in mail.iter_parts(): head = _get_mime_head_info(part) - if head["content_disposition"] != "attachment": + if ( + head.contentdispo + != jwebmail.MIMEHeader.ContentDisposition.CONTENT_DISPOSITION_ATTACHMENT + ): body = _get_body(part) else: body = None - ret["parts"].append( - { - "head": head, - "body": body, - } + ret.parts.append( + jwebmail.MIMEPart( + mime_header=head, + body=body, + ) ) - return ret + return jwebmail.MailBody(multipart=ret) else: raise ValueError(f"unknown major content-type {mctype!r}") -def read_mail(f, subfolder, mid): - if subfolder: - f = f.get_folder(subfolder) +def read_mail(f, req): + r = jwebmail.ShowReq() + r.ParseFromString(req) + + if r.folder: + f = f.get_folder(r.folder) - msg = f.get(mid, None) + msg = f.get(r.mid, None) if not msg: - raise QMAuthError("no such message", mid=mid) + raise QMAuthError("no such message", mid=r.mid) - return { - "head": _get_head_info(msg), - "body": _get_body(msg), - } + res = jwebmail.Mail( + head=_get_head_info(msg), + body=_get_body(msg), + ) + return jwebmail.ShowResp(mail=res).SerializeToString() def _descent(xx): head = _get_mime_head_info(xx) - if (mctype := head["content_maintype"]) == "message": + if (mctype := head.maintype) == "message": body = xx.get_content() elif mctype == "multipart": body = xx.iter_parts() else: body = xx.get_content() - return { - "head": head, - "body": body, - } + return head, body + +def raw_mail(f, req): + r = jwebmail.RawReq() + r.ParseFromString(req) -def raw_mail(f, subfolder, mid, path): - if subfolder: - f = f.get_folder(subfolder) + if r.folder: + f = f.get_folder(r.folder) - msg = f.get(mid, None) + msg = f.get(r.mid, None) if not msg: - raise QMAuthError("no such message", mid=mid) + raise QMAuthError("no such message", mid=r.mid) - pth = [int(seg) for seg in path.split(".")] if path else [] - mail = { - "head": {"content_maintype": "message", "content_subtype": "rfc822"}, - "body": msg, - } + pth = [int(seg) for seg in r.path.split(".")] if r.path else [] + h = jwebmail.MIMEHeader(maintype="message", subtype="rfc822") + b = msg for n in pth: - mctype = mail["head"]["content_maintype"] + mctype = h.maintype if mctype == "multipart": try: - res = next(islice(mail["body"], n, None)) + res = next(islice(b, n, None)) except StopIteration: raise QMAuthError("out of bounds path for mail", path=pth) - mail = _descent(res) + (h, b) = _descent(res) elif mctype == "message": assert n == 0 - mail = _descent(mail["body"]) + (h, b) = _descent(b) else: raise QMAuthError( f"can not descent into non multipart content type {mctype}" ) - if hasattr(mail["body"], "__next__"): + if hasattr(b, "__next__"): raise QMAuthError("can not stop at multipart section", path=pth) - json.dump(mail["head"], stdout) - stdout.write("\n") - if isinstance(mail["body"], str): - stdout.write(mail["body"]) - elif isinstance(mail["body"], bytes): - stdout.flush() - stdout.buffer.write(mail["body"]) - else: - stdout.write(str(mail["body"])) - sysexit(0) + if isinstance(b, str): + b = b.encode() + + return jwebmail.RawResp(header=h, body=b).SerializeToString() def _matches(m, pattern): @@ -349,51 +383,83 @@ def _matches(m, pattern): 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) +def search_mails(f, req): + r = jwebmail.SearchReq() + r.ParseFromString(req) - return [ - { - "head": _get_head_info(msg), - "body": _get_body(msg), - } + if r.folder: + f = f.get_folder(r.folder) + + res = [ + jwebmail.ListMailHeader( + header=_get_head_info(msg), + ) for msg in f.values() - if _matches(msg, pattern) + if _matches(msg, r.pattern) ] + return jwebmail.SearchResp(found=res).SerializeToString() -def folders(f): - return f.list_folders() +def folders(f, req): + r = jwebmail.FoldersReq() + r.ParseFromString(req) + return jwebmail.FoldersResp(folders=f.list_folders()).SerializeToString() -def move_mail(f, mid, from_, to): - if from_: - f = f.get_folder(from_) +def move_mail(f, req): + r = jwebmail.MoveReq() + r.ParseFromString(req) - fname = Path(f.get_filename(mid)) + if r.from_f: + f = f.get_folder(r.from_f) - assert to in f.list_folders() + fname = Path(f.get_filename(r.mid)) - sep = -2 if not from_ else -3 + assert r.to_f in f.list_folders() - if to: - res = fname.parts[:sep] + ("." + to,) + fname.parts[-2:] + sep = -2 if not r.from_f else -3 + + if r.to_f: + res = fname.parts[:sep] + ("." + r.to_f,) + fname.parts[-2:] else: res = fname.parts[:sep] + fname.parts[-2:] fname.rename(Path(*res)) - return 1 + return jwebmail.MoveResp().SerializeToString() + +def remove_mail(f, req): + r = jwebmail.RemoveReq() + r.ParseFromString(req) -def remove_mail(f, subdir, mid): - if subdir: - f = f.get_folder(subdir) + if r.folder: + f = f.get_folder(r.folder) - f[mid].add_flag("T") + f[r.mid].add_flag("T") - return 1 + return jwebmail.RemoveResp().SerializeToString() + + +def method_to_run(value): + if value == "list": + return list_mails + elif value == "count": + return count_mails + elif value == "read": + return read_mail + elif value == "raw": + return raw_mail + elif value == "folders": + return folders + elif value == "move": + return move_mail + elif value == "remove": + return remove_mail + elif value == "search": + return search_mails + else: + raise ValueError(value) def parse_arguments(): @@ -402,48 +468,9 @@ def parse_arguments(): 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_raw = sp.add_parser("raw") - sp_raw.add_argument("subfolder") - sp_raw.add_argument("mid", metavar="message") - sp_raw.add_argument("path", default="") - sp_raw.set_defaults(run=raw_mail) - - sp_folders = sp.add_parser("folders") - sp_folders.set_defaults(run=folders) - - 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_remove = sp.add_parser("remove") - sp_remove.add_argument("subdir") - sp_remove.add_argument("mid", metavar="message") - sp_remove.set_defaults(run=remove_mail) - - sp_search = sp.add_parser("search") - sp_search.add_argument("pattern") - sp_search.add_argument("subfolder") - sp_search.set_defaults(run=search_mails) + ap.add_argument( + "method", choices=["list", "count", "read", "raw", "folders", "move", "remove"] + ) return vars(ap.parse_args()) @@ -457,19 +484,22 @@ def main(): args = parse_arguments() logging.debug("started with %s", args) s = startup( - args.pop("maildir_path"), - args.pop("os_user"), - args.pop("mail_user"), - args["run"], + args["maildir_path"], + args["os_user"], + args["mail_user"], + args["method"], ) logging.debug("setuid successful") - run = args.pop("run") - reply = run(s, **args) - json.dump(reply, stdout) - except QMAuthError as qerr: - errmsg = dict(error=qerr.msg, **qerr.info) - json.dump(errmsg, stdout) - sysexit(3) + stdout.write("OPEN\n") + stdout.flush() + val = stdin.buffer.read() + run = method_to_run(args["method"]) + reply = run(s, val) + logging.debug("pb method(%s) size(%d)", args["method"], len(reply)) + stdout.buffer.write(reply) + #except QMAuthError as qerr: + # errmsg = dict(error=qerr.msg, **qerr.info) + # sysexit(3) except Exception: logging.exception("qmauth.py error") sysexit(4) |