summaryrefslogtreecommitdiff
path: root/script
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2024-11-19 23:15:55 +0100
committerJannis M. Hoffmann <jannis@fehcom.de>2024-11-19 23:15:55 +0100
commit5324a38f8fbd41391741317f7e7ab2d69ec30623 (patch)
tree7a6ff48bb8dece79ac782977e510bb5bd048fc21 /script
parentae117909d9f39103e32296325e93fa3c22749350 (diff)
switch from protobuf based protocol to varlink
Diffstat (limited to 'script')
-rwxr-xr-xscript/extract.py805
-rwxr-xr-xscript/moveto3.py28
2 files changed, 406 insertions, 427 deletions
diff --git a/script/extract.py b/script/extract.py
index c670bef..f311fba 100755
--- a/script/extract.py
+++ b/script/extract.py
@@ -5,47 +5,31 @@
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.
-
-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::
-
- 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
+This program is started by qmail-authuser with elevated privileges after a
+successful login.
"""
import email.parser
import email.policy
import logging
import re
-from argparse import ArgumentParser
+import socket
from base64 import b64encode
from datetime import datetime
from email.message import EmailMessage
from itertools import islice
-from mailbox import Maildir, MaildirMessage
-from os import environ, getpid, mkdir, path, setuid
+from mailbox import Maildir, MaildirMessage, NoSuchMailboxError
+from os import environ, getpid, mkdir, path, setuid, stat
from pathlib import Path
from pwd import getpwnam
from sys import exit as sysexit
-from sys import stdin, stdout
-import jwebmail.model.jwebmail_pb2 as jwebmail
+import varlink
class MyMaildir(Maildir):
def __init__(self, dirname, parent=None, *args, **kwargs):
self.__path = Path(dirname)
- self.__parent = parent
self.set_msgtype("MaildirMessage")
super().__init__(dirname, *args, **kwargs)
@@ -96,16 +80,9 @@ class MyMaildir(Maildir):
"""
return type(self)(
path.join(self._path, "." + folder),
- parent=self,
create=False,
)
- def list_folders(self):
- if self.__parent is not None:
- return self.__parent.list_folders()
- else:
- return super().list_folders()
-
def _refresh(self):
"""
This override of internal method _refresh that strips out 'hidden' files.
@@ -116,427 +93,462 @@ class MyMaildir(Maildir):
del self._toc[r]
-class QMAuthError(Exception):
- def __init__(self, msg, **args):
- self.msg = msg
- self.info = args
+service = varlink.Service(
+ vendor="JWebmail",
+ product="Mail Storage Interface",
+ version="1",
+ url="https://www.fehcom.de/cgit/jwebmail2",
+ interface_dir=path.join(path.dirname(__file__), "../src/jwebmail/model/"),
+)
-def _adr(addrs):
- if addrs is None:
- return []
- return [
- jwebmail.MailHeader.MailAddr(
- address=addr.addr_spec,
- name=addr.display_name,
+class InvalidUserError(varlink.VarlinkError):
+
+ def __init__(self, unix_user):
+ super().__init__(
+ {
+ "error": "de.jmhoffmann.jwebmail.mail-storage.InvalidUser",
+ "parameters": {
+ "unix_user": unix_user,
+ },
+ }
)
- for addr in addrs.addresses
- ]
-def _get_rcv_time(mid):
- idx = mid.find(".")
- assert idx > 0
- return float(mid[:idx])
+class NotInitializedError(varlink.VarlinkError):
+ def __init__(self):
+ super().__init__(
+ {"error": "de.jmhoffmann.jwebmail.mail-storage.NotInitialized"}
+ )
-def startup(maildir, su, user, mode):
- del environ["PATH"]
- netfehcom_uid = getpwnam(su).pw_uid
- if not netfehcom_uid:
- logging.error("user must not be root")
- sysexit(5)
- try:
- setuid(netfehcom_uid)
- except OSError:
- logging.exception("error setting uid")
- sysexit(5)
-
- return MyMaildir(maildir / user, create=False)
-
-
-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:]
-
- def by_rec_date(midmsg):
- return 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":
- # fmt: off
- def keyfn(midmsg): return midmsg[1]["subject"]
- # fmt: on
- elif sort == "size":
- # fmt: off
- def keyfn(midmsg): return path.getsize(f.get_filename(midmsg[0]))
- # fmt: on
- 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):
- 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
+class InvalidMailboxError(varlink.VarlinkError):
+
+ def __init__(self, path, not_a_mailbox, user_mismatch):
+ super().__init__(
+ {
+ "error": "de.jmhoffmann.jwebmail.mail-storage.InvalidMailbox",
+ "parameters": {
+ "path": path,
+ "not_a_mailbox": not_a_mailbox,
+ "user_mismatch": user_mismatch,
+ },
+ }
)
- else:
- assert False
- if fn := msg.get_filename():
- mh.file_name = fn
- return mh
+class InvalidMIDError(varlink.VarlinkError):
+ def __init__(self, folder, mid):
+ super().__init__(
+ {
+ "error": "de.jmhoffmann.jwebmail.mail-storage.InvalidMID",
+ "parameters": {
+ "folder": folder,
+ "mid": mid,
+ },
+ }
+ )
-def _get_head_info(msg):
- 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]
+class InvalidPathInMailError(varlink.VarlinkError):
- return mh
+ def __init__(self, folder, mid, path):
+ super().__init__(
+ {
+ "error": "de.jmhoffmann.jwebmail.mail-storage.InvalidPathInMail",
+ "parameters": {
+ "folder": folder,
+ "mid": mid,
+ "path": path,
+ },
+ }
+ )
-def list_mails(f, req):
- r = jwebmail.ListReq()
- r.ParseFromString(req)
+@service.interface("de.jmhoffmann.jwebmail.mail-storage")
+class MailStorage:
- assert 0 <= r.start <= r.end
+ def __init__(self):
+ self.maildir = None
- if r.folder:
- f = f.get_folder(r.folder)
+ def Init(self, su, maildir):
+ del environ["PATH"]
- f.set_msgtype("Maildir(EmailMessageHeader)")
+ try:
+ uid = getpwnam(su).pw_uid
+ if not uid:
+ raise InvalidUserError(su)
- if r.start == r.end:
- return []
+ setuid(uid)
- kfn, reverse = _sort_mails(f, r.sort)
- msgs = list(f.items())
- msgs.sort(key=kfn, reverse=reverse)
- msgs = msgs[r.start : min(len(msgs), r.end)]
+ if stat(maildir).st_uid != uid:
+ raise InvalidMailboxError(
+ maildir, not_a_mailbox=False, user_mismatch=True
+ )
- items = [
- jwebmail.ListMailHeader(
- mid=mid,
- byte_size=path.getsize(f.get_filename(mid)),
- unread="S" not in msg.get_flags(),
- rec_date=datetime.fromtimestamp(_get_rcv_time(mid)).isoformat(),
- header=_get_head_info(msg),
+ self.maildir = MyMaildir(maildir, create=False)
+ except KeyError:
+ raise InvalidUserError(su)
+ except (FileNotFoundError, NoSuchMailboxError):
+ raise InvalidMailboxError(maildir, not_a_mailbox=True, user_mismatch=False)
+
+ def List(self, folder, start, end, sort):
+ if self.maildir is None:
+ raise NotInitializedError()
+
+ assert 0 <= start <= end
+
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
+
+ maildir.set_msgtype("Maildir(EmailMessageHeader)")
+
+ if start == end:
+ return []
+
+ kfn, reverse = self._sort_mails(maildir, sort)
+ msgs = list(maildir.items())
+ msgs.sort(key=kfn, reverse=reverse)
+ msgs = msgs[start : min(len(msgs), end)]
+
+ def _get_rcv_time(mid):
+ return float(mid[: mid.index(".")])
+
+ items = [
+ {
+ "mid": mid,
+ "byte_size": path.getsize(maildir.get_filename(mid)),
+ "unread": "S" not in msg.get_flags(),
+ "rec_date": datetime.fromtimestamp(_get_rcv_time(mid)).isoformat(),
+ "header": self._get_head_info(msg),
+ }
+ for mid, msg in msgs
+ ]
+ return {"mail_heads": items}
+
+ def Stats(self, folder):
+ if self.maildir is None:
+ raise NotInitializedError()
+
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
+
+ maildir.set_msgtype("None")
+
+ return dict(
+ mail_count=len(maildir),
+ unread_count=len([1 for m in maildir if "S" in m.get_flags()]),
+ byte_size=sum(
+ path.getsize(maildir.get_filename(mid)) for mid in maildir.keys()
+ ),
)
- for mid, msg in msgs
- ]
- return jwebmail.ListResp(mail_heads=items).SerializeToString()
+ def Show(self, folder, mid):
+ if self.maildir is None:
+ raise NotInitializedError()
-def count_mails(f, req):
- r = jwebmail.StatsReq()
- r.ParseFromString(req)
- if r.folder:
- f = f.get_folder(r.folder)
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
- f.set_msgtype("None")
+ maildir.set_msgtype("EmailMessage")
- 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()
+ msg = maildir.get(mid, None)
+ if not msg:
+ raise InvalidMIDError(folder, mid)
+ maildir.set_msgtype("MaildirMessage")
-def _get_body(mail):
- if not mail.is_multipart():
- if mail.get_content_maintype() == "text":
- return jwebmail.MailBody(discrete=mail.get_content())
- else:
- ret = mail.get_content()
- if ret.isascii():
- return jwebmail.MailBody(discrete=ret.decode(encoding="ascii"))
- elif len(ret) <= 128 * 1024:
- return jwebmail.MailBody(
- discrete=b64encode(ret).decode(encoding="ascii")
- )
- else:
- raise QMAuthError(
- "non attachment part too large (>512kB)", size=len(ret)
- )
+ maildir[mid].add_flag("S")
- if (mctype := mail.get_content_maintype()) == "message":
- msg = mail.get_content()
- return jwebmail.MailBody(
- mail=jwebmail.Mail(head=_get_head_info(msg), body=_get_body(msg))
- )
- elif mctype == "multipart":
- ret = jwebmail.MailBody.Multipart(
- preamble=mail.preamble,
- epilogue=mail.epilogue,
- )
- for part in mail.iter_parts():
- head = _get_mime_head_info(part)
- if (
- head.contentdispo
- != jwebmail.MIMEHeader.ContentDisposition.CONTENT_DISPOSITION_ATTACHMENT
- ):
- body = _get_body(part)
+ return {
+ "mail": dict(
+ head=self._get_head_info(msg),
+ body=self._get_body(msg),
+ )
+ }
+
+ def Raw(self, folder, mid, path):
+ if self.maildir is None:
+ raise NotInitializedError()
+
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
+
+ maildir.set_msgtype("EmailMessage")
+
+ msg = maildir.get(mid, None)
+ if not msg:
+ raise InvalidMIDError(folder, mid)
+
+ pth = [int(seg) for seg in path.split(".")] if path else []
+ h = dict(mime_type={"main_type": "message", "sub_type": "rfc822"})
+ b = msg
+
+ for n in pth:
+ mctype = h["mime_type"]["main_type"]
+
+ if mctype == "multipart":
+ try:
+ res = next(islice(b, n, None))
+ except StopIteration:
+ raise InvalidPathInMailError(folder, mid, pth)
+ (h, b) = self._descent(res)
+ elif mctype == "message":
+ assert n == 0
+ (h, b) = self._descent(b)
else:
- body = None
- ret.parts.append(
- jwebmail.MIMEPart(
- mime_header=head,
- body=body,
+ raise InvalidPathInMailError(
+ f"can not descent into non multipart content type {mctype}"
)
- )
- return jwebmail.MailBody(multipart=ret)
- else:
- raise ValueError(f"unknown major content-type {mctype!r}")
-
-
-def read_mail(f, req):
- r = jwebmail.ShowReq()
- r.ParseFromString(req)
- if r.folder:
- f = f.get_folder(r.folder)
+ if hasattr(b, "__next__"):
+ # can not stop at multipart section
+ raise InvalidPathInMailError(folder=folder, mid=mid, path=pth)
+ elif isinstance(b, str):
+ b = b.encode()
+ elif isinstance(b, EmailMessage):
+ b = b.as_bytes()
- f.set_msgtype("EmailMessage")
+ return dict(header=h, body=b64encode(b).decode("ASCII"))
- msg = f.get(r.mid, None)
- if not msg:
- raise QMAuthError("no such message", mid=r.mid)
+ def Search(self, folder, pattern):
+ if self.maildir is None:
+ raise NotInitializedError()
- f.set_msgtype("MaildirMessage")
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
- f[r.mid].add_flag("S")
+ maildir.set_msgtype("EmailMessage")
- res = jwebmail.Mail(
- head=_get_head_info(msg),
- body=_get_body(msg),
- )
- return jwebmail.ShowResp(mail=res).SerializeToString()
+ res = [
+ dict(header=self._get_head_info(msg))
+ for msg in maildir.values()
+ if self._matches(msg, pattern)
+ ]
+ return {"found": res}
+ def Folders(self):
+ if self.maildir is None:
+ raise NotInitializedError()
-def _descent(xx):
- head = _get_mime_head_info(xx)
- if (mctype := head.maintype) == "message":
- body = xx.get_content()
- elif mctype == "multipart":
- body = xx.iter_parts()
- else:
- body = xx.get_content()
- return head, body
+ return {"folders": self.maildir.list_folders()}
+ def Move(self, mid, from_folder, to_folder):
+ if self.maildir is None:
+ raise NotInitializedError()
-def raw_mail(f, req):
- r = jwebmail.RawReq()
- r.ParseFromString(req)
+ maildir = self.maildir
+ if from_folder:
+ maildir = maildir.get_folder(from_folder)
- if r.folder:
- f = f.get_folder(r.folder)
+ fname = Path(maildir.get_filename(mid))
- f.set_msgtype("EmailMessage")
+ assert to_folder in self.maildir.list_folders() or to_folder == ""
- msg = f.get(r.mid, None)
- if not msg:
- raise QMAuthError("no such message", mid=r.mid)
+ sep = -2 if not from_folder else -3
- 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 = h.maintype
-
- if mctype == "multipart":
- try:
- res = next(islice(b, n, None))
- except StopIteration:
- raise QMAuthError("out of bounds path for mail", path=pth)
- (h, b) = _descent(res)
- elif mctype == "message":
- assert n == 0
- (h, b) = _descent(b)
+ if to_folder:
+ res = fname.parts[:sep] + ("." + to_folder,) + fname.parts[-2:]
else:
- raise QMAuthError(
- f"can not descent into non multipart content type {mctype}"
- )
+ res = fname.parts[:sep] + fname.parts[-2:]
- if hasattr(b, "__next__"):
- raise QMAuthError("can not stop at multipart section", path=pth)
- elif isinstance(b, str):
- b = b.encode()
- elif isinstance(b, EmailMessage):
- b = b.as_bytes()
+ fname.rename(Path(*res))
- return jwebmail.RawResp(header=h, body=b).SerializeToString()
+ def Remove(self, folder, mid):
+ if self.maildir is None:
+ raise NotInitializedError()
+ maildir = self.maildir
+ if folder:
+ maildir = maildir.get_folder(folder)
-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)
+ maildir.set_msgtype("MaildirMessage")
+ maildir[mid].add_flag("T")
-def search_mails(f, req):
- r = jwebmail.SearchReq()
- r.ParseFromString(req)
+ def AddFolder(self, name):
+ if self.maildir is None:
+ raise NotInitializedError()
- if r.folder:
- f = f.get_folder(r.folder)
-
- f.set_msgtype("EmailMessage")
-
- res = [
- jwebmail.ListMailHeader(
- header=_get_head_info(msg),
+ name = path.join(
+ self.maildir._path, "." + name.translate(str.maketrans("/", "."))
)
- for msg in f.values()
- if _matches(msg, r.pattern)
- ]
- return jwebmail.SearchResp(found=res).SerializeToString()
-
-def folders(f, req):
- r = jwebmail.FoldersReq()
- r.ParseFromString(req)
- return jwebmail.FoldersResp(folders=f.list_folders()).SerializeToString()
-
-
-def move_mail(f, req):
- r = jwebmail.MoveReq()
- r.ParseFromString(req)
-
- if r.from_f:
- f = f.get_folder(r.from_f)
-
- fname = Path(f.get_filename(r.mid))
-
- assert r.to_f in f.list_folders() or r.to_f == ""
-
- 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 jwebmail.MoveResp().SerializeToString()
-
-
-def remove_mail(f, req):
- r = jwebmail.RemoveReq()
- r.ParseFromString(req)
-
- if r.folder:
- f = f.get_folder(r.folder)
-
- f.set_msgtype("MaildirMessage")
+ if path.isdir(name):
+ return {"status": "skipped"}
+
+ mkdir(name)
+ mkdir(path.join(name, "cur"))
+ mkdir(path.join(name, "new"))
+ mkdir(path.join(name, "tmp"))
+
+ return {"status": "created"}
+
+ @staticmethod
+ def _adr(addrs):
+ if addrs is None:
+ return []
+
+ ret = []
+ for addr in addrs.addresses:
+ struct = {"address": addr.addr_spec}
+ if addr.display_name:
+ struct["name"] = addr.display_name
+ ret.append(struct)
+ return ret
+
+ @staticmethod
+ def _get_mime_head_info(msg):
+ mh = dict(
+ mime_type={
+ "main_type": msg.get_content_maintype(),
+ "sub_type": msg.get_content_subtype(),
+ }
+ )
+ if (cd := msg.get_content_disposition()) == "inline":
+ mh["content_dispo"] = "inline"
+ elif cd == "attachment":
+ mh["content_dispo"] = "attachment"
+ elif cd is None:
+ mh["content_dispo"] = "none"
+ else:
+ assert False
+
+ if fn := msg.get_filename():
+ mh["file_name"] = fn
+
+ return mh
+
+ def _get_head_info(self, msg):
+ mh = dict(
+ send_date=msg["date"].datetime.isoformat(),
+ written_from=self._adr(msg["from"]),
+ reply_to=self._adr(msg["reply-to"]),
+ send_to=self._adr(msg["to"]),
+ cc=self._adr(msg["cc"]),
+ bcc=self._adr(msg["bcc"]),
+ subject=msg["subject"],
+ comments=msg["comments"],
+ keywords=msg["keywords"],
+ mime=self._get_mime_head_info(msg),
+ )
- f[r.mid].add_flag("T")
+ if s := self._adr(msg["sender"]):
+ mh.sender = s[0]
- return jwebmail.RemoveResp().SerializeToString()
+ return mh
+ def _get_body(self, mail):
+ if not mail.is_multipart():
+ if mail.get_content_maintype() == "text":
+ return dict(discrete=mail.get_content())
+ else:
+ ret = mail.get_content()
+ if ret.isascii():
+ return dict(discrete=ret.decode(encoding="ASCII"))
+ elif len(ret) <= 128 * 1024:
+ return dict(discrete=b64encode(ret).decode(encoding="ASCII"))
+ else:
+ raise ValueError(
+ "non attachment part too large (>512kB)", size=len(ret)
+ )
-def add_folder(f, req):
- r = jwebmail.AddFolderReq()
- r.ParseFromString(req)
+ if (mctype := mail.get_content_maintype()) == "message":
+ msg = mail.get_content()
+ return dict(
+ mail=dict(head=self._get_head_info(msg), body=self._get_body(msg))
+ )
+ elif mctype == "multipart":
+ ret = dict(
+ preamble=mail.preamble,
+ parts=[],
+ epilogue=mail.epilogue,
+ )
+ for part in mail.iter_parts():
+ head = self._get_mime_head_info(part)
+ if head["content_dispo"] != "attachment":
+ body = self._get_body(part)
+ else:
+ body = None
+ ret["parts"].append(dict(mime_header=head, body=body))
+ return dict(multipart=ret)
+ else:
+ raise ValueError(f"unknown major content-type {mctype!r}")
+
+ def _descent(self, xx):
+ head = self._get_mime_head_info(xx)
+ if (mctype := head["mime_type"]["main_type"]) == "message":
+ body = xx.get_content()
+ elif mctype == "multipart":
+ body = xx.iter_parts()
+ else:
+ body = xx.get_content()
+ return head, body
+
+ @staticmethod
+ 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)
- name = path.join(f._path, "." + r.name.translate(str.maketrans("/", ".")))
+ @staticmethod
+ def _sort_mails(f, sort):
- if path.isdir(name):
- return jwebmail.AddFolderResp(status=1).SerializeToString()
+ match sort["direction"]:
+ case "asc":
+ reverse = False
+ case "desc":
+ pass
+ reverse = True
+ case sort_direct:
+ raise ValueError(f"unknown sort direction {sort_direct!r}")
- mkdir(name)
- mkdir(path.join(name, "cur"))
- mkdir(path.join(name, "new"))
- mkdir(path.join(name, "tmp"))
+ def _sort_by_sender(midmsg):
+ _, msg = midmsg
- return jwebmail.AddFolderResp(status=0).SerializeToString()
+ if len(addrs := msg["from"].addresses) == 1:
+ return addrs[0].addr_spec
+ else:
+ return msg["sender"].address.addr_spec
+ def by_rec_date(midmsg):
+ return float(re.match(r"\d+\.\d+", midmsg[0], re.ASCII)[0])
-def method_to_run(value):
- match value:
- case "list":
- return list_mails
- case "count":
- return count_mails
- case "read":
- return read_mail
- case "raw":
- return raw_mail
- case "folders":
- return folders
- case "move":
- return move_mail
- case "remove":
- return remove_mail
- case "search":
- return search_mails
- case "add-folder":
- return add_folder
- case _:
- raise ValueError(value)
+ match sort["parameter"]:
+ case "date":
+ keyfn = by_rec_date
+ case "sender":
+ keyfn = _sort_by_sender
+ case "subject":
+ # fmt: off
+ def keyfn(midmsg): return midmsg[1]["subject"]
+ # fmt: on
+ case "size":
+ # fmt: off
+ def keyfn(midmsg): return path.getsize(f.get_filename(midmsg[0]))
+ # fmt: on
+ case sort_param:
+ logging.warning("unknown sort-verb %r", sort_param)
+ reverse = False
+ keyfn = by_rec_date
+ return keyfn, reverse
-def parse_arguments():
- ap = ArgumentParser(allow_abbrev=False)
- ap.add_argument("maildir_path", type=Path)
- ap.add_argument("os_user")
- ap.add_argument("mail_user")
- ap.add_argument("method", type=method_to_run)
- return vars(ap.parse_args())
+class RequestHandler(varlink.RequestHandler):
+ service = service
def main():
@@ -545,25 +557,20 @@ def main():
level="INFO",
format="%(levelname)s:" + str(getpid()) + ":%(message)s",
)
- args = parse_arguments()
- logging.debug("started with %s", args)
- s = startup(
- args["maildir_path"],
- args["os_user"],
- args["mail_user"],
- args["method"],
- )
- logging.debug("setuid successful")
- stdout.write("OPEN\n")
- stdout.flush()
- val = stdin.buffer.read()
- 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)
+
+ if environ["LISTEN_FDS"] == "1":
+ sok = socket.fromfd(3, socket.AF_UNIX, socket.SOCK_STREAM)
+ RequestHandler(sok, None, None)
+ return
+
+ types = environ["LISTEN_FDNAMES"].split(":")
+ assert len(types) == int(environ["LISTEN_FDS"])
+
+ for i, typ in enumerate(types):
+ if typ == "varlink":
+ sok = socket.fromfd(3 + i, socket.AF_UNIX, socket.SOCK_STREAM)
+ RequestHandler(sok, None, None)
+ sok.close()
except Exception:
logging.exception("qmauth.py error")
sysexit(4)
diff --git a/script/moveto3.py b/script/moveto3.py
deleted file mode 100755
index 408ff1a..0000000
--- a/script/moveto3.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import os
-
-
-def main():
- ap = argparse.ArgumentParser()
- ap.add_argument("-a", default="qmail-authuser", dest="pam")
- ap.add_argument("fd", type=int)
- ap.add_argument("prog")
- ap.add_argument("args", nargs="*")
-
- vals = ap.parse_args()
-
- if vals.fd < 3:
- raise ValueError(f"fd({vals.fd}) must be 3 or greater")
-
- if vals.fd != 3:
- os.dup2(vals.fd, 3)
- os.close(vals.fd)
-
- os.execvp(vals.pam, [vals.pam, vals.prog] + vals.args)
-
- raise ValueError("should not be reachable")
-
-
-if __name__ == "__main__":
- main()