On Sat, May 17, 2025 at 04:39:07PM +0200, Sébastien Hinderer wrote:
> Hello Chris, thanks a lot for your feedback!
>
> Chris Green (2025/05/17 14:39 +0100):
> > On Sat, May 17, 2025 at 12:49:16PM +0200, Sébastien Hinderer wrote:
> > > For instance here is the configuration I have for this list:
> > >
> > > subscribe [email protected]
> > > fcc-hook '~t [email protected]|~c [email protected]'
> > > '=list/mutt-users/'
> > > save-hook '~t [email protected]|~c [email protected]'
> > > '=list/mutt-users/'
> > >
> > > It's definitely not unbearable, but each time I add a mailing list and
> > > duplicate such lines I feel a bit sad about the redundancy, to the point
> > > that I regularly find myself thinking that one day I will write a
> > > generator to generate my Mutt configuration from a less verbose format.
> > >
> > Yes. What I do is have a configuration file in which I have all my
> > lists and this is used to autogenerate the required subscribe and
> > other mutt configuration.
>
> Yes it's what I said I intended to do. You may want to share you
> rscript!
>
I think I have done before but see attached files anyway.
The mutt configuration works using two very simple Python scripts as
follows (I've attached these two scripts as well) :-
#
#
# Mailing lists, see ~/.mutt/filter and ~/mutt/bin/filter.py for details.
#
lists `~/.mutt/bin/getLists.py`
subscribe `~/.mutt/bin/getLists.py`
#
#
# getAliases.py gets aliases for mailing lists from the filter file
#
source ~/.mutt/bin/getAliases.py|
Here's a section of the file 'filter' which is what I change to add or
remove mailing lists (and some other things):-
letsencrypt Li list-id help.community.letsencrypt.org
mercurial Li list-post [email protected]
mercurial Li list-post [email protected]
mopidy Li list-id q&a.discourse.mopidy.com
mutt Li list-post [email protected]
netman Li list-post [email protected]
owfs Li list-post [email protected]
Owfs-developers
openLibrary Li list-post [email protected]
photini Li list-post [email protected]
postfix Li list-post [email protected]
pfx
quodlibet Li list-post [email protected]
> > The same file is used by a Python script
> > that routes incoming list mail to separate mailboxes.
>
> Which is the part I would not keep, to save me from not reading most of
> my e-mails ahah. :) But the script could also generate procmail rules...
>
My Python script does what procmail does, no need for procmail as well.
> Best wishes,
>
> Seb.
>
--
Chris Green
#!/usr/bin/python3
#
#
# license Apache v2 (http://www.apache.org/licenses/LICENSE-2.0)
# author Chris Green - [email protected]
#
#
#
# Mail filtering script
#
import mailbox
import os
import sys
import time
import mailLib
import shlex
#
#
# Redirect any exceptions to a file
#
sys.stderr = open("/home/chris/tmp/mail.err", 'a')
#
#
# Some constants (i.e. configuration)
#
home = "/home/chris"
logfile = home + "/tmp/mail.log"
filtfile = home + "/.mutt/filter"
mldir = home + "/mail/"
indir = mldir + "In/"
#
#
# Set to log to mail.log in ~/tmp with name 'filter' and the envelope/from
#
log = mailLib.initLog("filter")
#
#
# Initialise destination mailbox name to its default "In/default"
#
dest = indir + "default"
#
#
# Read the message from standard input and make a message object from it
#
msg = mailbox.MaildirMessage(sys.stdin.buffer.read())
#
#
# See if there's a match in our filter file
#
f = open(filtfile, 'r')
for ln in f: # for each line in filter
if ln[0] == '#': # ignore comments
continue
#
#
# split the line into fields, shlex.split() does quoted strings
#
fld = shlex.split(ln)
#
#
# copy the fields into better named variables
#
nm = fld[0] # name/alias
destdir = fld[1] + "/" # the destination directory
header = fld[2] # the header to find the match in
address = fld[3].lower() # list address to match
if len(fld) == 5: # if there's a 5th field
sbstrip = '[' + fld[4] + ']' # string to strip out of subject
dbasbstrip = '(' + fld[4] + ')' # string to strip from DBA subject
#
#
# Does the address in the header match this entry
#
if (address in str(msg.get(header, "unknown").lower())):
#
#
# set the destination directory
#
if nm[0:3] == "dba":
#
#
# For some reason when the Subject: has accented characters in it the returned
# utf-8 string has spaces replaced by underscores, so we need to change them back
#
subject = msg.get("subject", "unknown").replace("_", " ")
#
#
# For DBA messages strip out the (<forum section>) and put the
# destination in From: and change To: so we can get the DBA alias when replying
#
if len(fld) == 5 and dbasbstrip in subject:
msg.replace_header("Subject", str(subject.replace(dbasbstrip, '')))
dest = mldir + destdir + nm
msg.replace_header("From", dest)
msg.replace_header("To", "barges.org")
break # match found so stop searching
else:
dest = mldir + destdir + nm
#
#
# Strip out list name (4th field) from subject if it's there
#
if len(fld) == 5 and sbstrip in str(msg.get("subject", "unknown")):
msg.replace_header("Subject", str(msg.get("subject").replace(sbstrip, '')))
break # match found so stop searching
#
#
# match not found so continue the for loop
#
#
#
# deliver the message
#
mailLib.spamFilter(msg)
mailLib.smsFilter(msg)
mailLib.deliverMdMsg(dest, msg, log)
#!/usr/bin/python3
#
#
# license Apache v2 (http://www.apache.org/licenses/LICENSE-2.0)
# author Chris Green - [email protected]
#
#
#
# Get mailing list addresses from filter file to provide mutt aliases
#
import sys
#
#
# Set directories and filenames
#
home = "/home/chris"
filtfile = home + "/.mutt/filter"
#
#
# Read from 'filter' line by line
#
f = open(filtfile, 'r')
for ln in f:
if ln[0] == '#': # ignore comments
continue
#
#
# split the line into fields
#
fld = ln.split()
#
#
# Only write aliases for mailing lists
#
if ((fld[1] == "Li") or (fld[1] == "Gm")):
sys.stdout.write("alias ")
sys.stdout.write(fld[0] + " ")
#
#
# List-Id matches need the list name converted to an E-Mail address
#
if (fld[2].lower() == "list-id"):
sys.stdout.write(fld[3].replace(".", "@", 1) + "\n")
else:
sys.stdout.write(fld[3] + "\n")
#!/usr/bin/python3
#
#
# license Apache v2 (http://www.apache.org/licenses/LICENSE-2.0)
# author Chris Green - [email protected]
#
#
#
# Get mailing lists from filter for mutt 'lists' command
#
import sys
#
#
# Set directory and filename
#
home = "/home/chris"
filtfile = home + "/.mutt/filter"
#
#
# Read filter line by line
#
f = open(filtfile, 'r')
for ln in f:
if ln[0] == '#': # ignore comments
continue
#
#
# split the line into fields
#
fld = ln.split()
#
#
# output the address if 'Li' or 'Gm' in second column
#
if ((fld[1] == "Li") or (fld[1] == "Gm")):
#
#
# List-Id matches need the list name converted to an E-Mail address
#
if (fld[2].lower() == "list-id"):
sys.stdout.write(fld[3].replace(".", "@", 1) + " ")
else:
sys.stdout.write(fld[3] + " ")
#
#
# license Apache v2 (http://www.apache.org/licenses/LICENSE-2.0)
# author Chris Green - [email protected]
#
import mailbox
import logging
import logging.handlers
import os
import time
from email.header import decode_header
from datetime import datetime
#
#
# log a message
#
def initLog(name):
log = logging.getLogger(name)
log.setLevel(logging.DEBUG)
f = logging.handlers.RotatingFileHandler("/home/chris/tmp/mail.log", 'a', 1000000, 4)
f.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
f.setFormatter(formatter)
log.addHandler(f)
return log
#
#
# Deliver a message to a local maildir
#
def deliverMdMsg(dest, msg, log):
#
#
# Create the destination maildir instance
#
md = mailbox.Maildir(dest, factory=None)
log.info("\n From: " + str(msg.get("From", "unknown")) + "\n To:" + dest + "\n")
#
#
# Put the incoming message in the appropriate maildir
# No need to lock, it's a maildir
#
try:
md.add(msg)
except Exception as e:
log.info("Failed to store message:" + str(e))
return
#
#
# Get a message header as a string
#
def getHdr(msg, header):
hdr = str(msg.get(header, "empty"))
res = decode_header(hdr)[0][0]
return str("\n " + header + ": " + str(res))
#
#
# Spam filter, initially just for junk in BBB list
#
def spamFilter(msg):
if "@gmail.com" in msg.get("from", "unknown"):
if msg.get("to", "unknown") == "BeagleBone <[email protected]>":
dt = datetime.now().isoformat(timespec='minutes')
f = open("/home/chris/tmp/spam.log")
f.write(dt + msg.get("subject") + " from " + msg.get("from"))
f.close()
#
#
# Change the From: address on SMS->E-Mail messages from my AandA VOIP
#
def smsFilter(msg):
if "[email protected]" in msg.get("from", "unknown"):
if "+447906626760" in msg.get("from", "unknown"):
msg.replace_header("From", "SMS from Maxine <[email protected]>");
elif "+447906226675" in msg.get("from", "unknown"):
msg.replace_header("From", "SMS from Chris <[email protected]>");
elif "+447537170394" in msg.get("from", "unknown"):
msg.replace_header("From", "SMS from Chris <[email protected]>");
elif "+447780705062" in msg.get("from", "unknown"):
msg.replace_header("From", "SMS from Zelma <[email protected]>");