""" Sopel plugin: mlmmj mailing list management !list subscribe - subscribe to list@gumx.cc !list unsubscribe - unsubscribe !list members - show all subscribers !list send - send a message to the list as owner !list ls [n] - show latest n archive entries (default 5) !list rm - remove a message from the archive immediately """ import email.utils, glob, mailbox, os, subprocess from email.header import decode_header as _decode_hdr from email.utils import parseaddr, parsedate_to_datetime from sopel import plugin LIST_DIR = "/var/spool/mlmmj/list" LIST_ADDR = "list@gumx.cc" OWNER_ADDR = "ahmed@gumx.cc" ARCHIVE = os.path.join(LIST_DIR, "archive") ARCHIVE_GEN = "/home/ahmed/scripts/archive-gen.py" try: with open(os.path.join(LIST_DIR, 'control', 'prefix')) as _f: LIST_PREFIX = _f.read().strip() except FileNotFoundError: LIST_PREFIX = '' def _owner(bot, trigger): return trigger.nick == bot.settings.core.owner def _decode(s): if not s: return '' parts = _decode_hdr(s) out = [] for part, charset in parts: if isinstance(part, bytes): out.append(part.decode(charset or 'utf-8', errors='replace')) else: out.append(part) return ''.join(out) def _strip_prefix(subject): if LIST_PREFIX and subject.startswith(LIST_PREFIX): return subject[len(LIST_PREFIX):].lstrip() return subject def _regen(): subprocess.run( ["python3", ARCHIVE_GEN, LIST_DIR], capture_output=True ) @plugin.command("list") def cmd_list(bot, trigger): if not _owner(bot, trigger): return args = (trigger.group(2) or "").split() if not args: bot.say("usage: !list ") return action = args[0] if action == "subscribe": if len(args) < 2: bot.say("usage: !list subscribe ") return email_addr = args[1].strip() r = subprocess.run( ["/usr/bin/mlmmj-sub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"], capture_output=True, text=True ) bot.say(f"subscribed: {email_addr}" if r.returncode == 0 else f"error: {(r.stderr or r.stdout).strip()}") elif action == "unsubscribe": if len(args) < 2: bot.say("usage: !list unsubscribe ") return email_addr = args[1].strip() r = subprocess.run( ["/usr/bin/mlmmj-unsub", "-L", LIST_DIR, "-a", email_addr, "-s", "-f"], capture_output=True, text=True ) bot.say(f"unsubscribed: {email_addr}" if r.returncode == 0 else f"error: {(r.stderr or r.stdout).strip()}") elif action == "members": subs_dir = os.path.join(LIST_DIR, "subscribers.d") subs = [] if os.path.isdir(subs_dir): for fname in sorted(glob.glob(os.path.join(subs_dir, "*"))): with open(fname) as f: subs.extend(line.strip() for line in f if line.strip()) if subs: bot.say(f"{len(subs)} subscriber(s): " + ", ".join(subs)) else: bot.say("no subscribers") elif action == "send": if len(args) < 2: bot.say("usage: !list send ") return message = " ".join(args[1:]).strip() msg = ( f"From: {OWNER_ADDR}\r\n" f"To: {LIST_ADDR}\r\n" f"Subject: {message}\r\n" f"Date: {email.utils.formatdate(localtime=True)}\r\n" f"\r\n" f"{message}\r\n" ) r = subprocess.run( ["/usr/sbin/sendmail", "-f", OWNER_ADDR, LIST_ADDR], input=msg, capture_output=True, text=True ) bot.say("queued" if r.returncode == 0 else f"error: {(r.stderr or r.stdout).strip()}") elif action == "ls": n = 5 if len(args) >= 2: try: n = int(args[1]) except ValueError: bot.say("usage: !list ls [n]") return items = sorted( f for f in os.listdir(ARCHIVE) if os.path.isfile(os.path.join(ARCHIVE, f)) and not f.endswith('.html') ) if not items: bot.say("archive: empty") return for fname in reversed(items[-n:]): fpath = os.path.join(ARCHIVE, fname) with open(fpath, 'rb') as fh: msg = mailbox.Message(fh) subject = _strip_prefix(_decode(msg.get('Subject', '(no subject)'))) _, addr = parseaddr(_decode(msg.get('From', ''))) sender = addr or _decode(msg.get('From', '')) date_raw = msg.get('Date', '') try: date_str = parsedate_to_datetime(date_raw).strftime('%Y-%m-%d %H:%M') except Exception: date_str = date_raw[:16] if date_raw else '?' bot.say(f"{fname}: [{date_str}] {sender}: {subject}") elif action == "rm": if len(args) < 2: bot.say("usage: !list rm ") return fid = args[1].strip() fpath = os.path.join(ARCHIVE, fid) if not os.path.isfile(fpath): bot.say(f"rm: not found: {fid}") return os.remove(fpath) html_path = fpath + '.html' if os.path.isfile(html_path): os.remove(html_path) _regen() bot.say(f"removed: {fid}") else: bot.say("usage: !list ")