Quick mailing list using Sendmail and Python
Some days ago, I needed to create some kind of a little mailing list, so some friends of mine could communicate themselves using a single email address instead of filling up all the To: and Cc: fields.
At first, I thought about using aliases. It was pretty easy, just add a line like:
mailing: firstname.lastname@example.org, email@example.com, firstname.lastname@example.org, email@example.com
to the /etc/mail/aliases file, and use newaliases to rebuild the aliases database. That's a solution, and it works fine, as soon as anyone send an email to firstname.lastname@example.org, the mail will be redirected to all the addresses in the list. But that solution has a problem, when anyone would like to reply to an email from that alias, he/she/it would have to use the reply to all button in his/her/its MUA, if not, the answer would be sent only to the address in the From: header (the one that sent that email previously to the alias address.
So, using aliases, I solved half of the problem only. I searched through the Internet and I a look in both the Sendmail Book (AKA the bat book) and the Sendmail CookBook from O'reilly (both of them have some nice tricks about using aliases in Sendmail), but I didn't find what I was searching for.
Then, I remembered one thing I noticed in the aliases man page:
Command |command A command starts with a pipe symbol (|), it receives messages via stan- dard input.
mmm, it seems like emails sent to an alias address could be re-sent through a pipe to a given command... in fact (after reading a little bit about it) seems like Majordomo works that way...
#!/usr/local/bin/python # # Little script to re-send emails coming from sendmail # aliases # # Wu | http://www.e-shell.org # Mon Oct 29 21:34:34 CET 2007 import sys, email, smtplib try: raw_email = sys.stdin.read() except: raise SystemExit email_addresses = ['email@example.com', 'firstname.lastname@example.org', 'email@example.com', 'firstname.lastname@example.org'] email_replyto = 'email@example.com' try: tmpfile = file('/tmp/SendToMailing', 'w') tmpfile.write(raw_email) tmpfile.close() except: raise SystemExit try: email_file = file('/tmp/SendToMailing', 'r') email_data = email.message_from_file(email_file) email_file.close() except: raise SystemExit email_data['Reply-to'] = email_replyto try: smtp_conn = smtplib.SMTP() smtp_conn.connect() smtp_conn.sendmail(email_data['From'], email_addresses, email_data.as_string()) smtp_conn.close() except: raise SystemExit
Easy, isn't it?
Let me explain some things about the script. first:
This is needed for the script to be interpreted by the python interpreter correctly. Probably, you have see something like:
instead in some other python scripts. That will result in Sendmail complaining about some internal mailer errors while trying to execute the script, so don't use it here.
try: raw_email = sys.stdin.read() except: raise SystemExit
Ok, that one is easy, as we can read in the aliases man page, A command starts with a pipe symbol (|), **it receives messages via standard input*.* So, we have to read the standard input (stdin) to get the email message.
Once we have read the email, the script saves it in some random file somewhere in the filesystem (/tmp/SendToMailing? here), and then we use the email module from python to get the contents of that file and put them inside an email object :
try: tmpfile = file('/tmp/SendToMailing', 'w') tmpfile.write(raw_email) tmpfile.close() except: raise SystemExit try: email_file = file('/tmp/SendToMailing', 'r') email_data = email.message_from_file(email_file) email_file.close() except: raise SystemExit
Next, the proper Reply-To header is added to the email object, and finally we use the smtplib module to send the mail to its final destination (the list of addresses in the mailing list):
try: smtp_conn = smtplib.SMTP() smtp_conn.connect() smtp_conn.sendmail(email_data['From'], email_addresses, email_data.as_string()) smtp_conn.close() except: raise SystemExit
The script needs all the try: ... except: checks because of Sendmail. If the script doesn't exit normally (for example, if an error happened while open the file for writing the email message), Sendmail will abort the process with some ugly errors. So, to avoid that, I just added some failure-control in the critical points of the script.
Fine, now we have the script, but we have to tell Sendmail that it should send emails coming for firstname.lastname@example.org to that script. Let's edit again the /etc/mail/aliases file, changing our mailing alias:
By default since one of the 8.11.x version, Sendmail uses a built-in shell to limit access from Sendmail to system-wide binaries. This shell is smrsh (Sendmail Restricted Shell). That means that only allowed binaries could be executed from Sendmail directly (as we are trying to do here). To add our little script to the allowed binaries, we have to create a symlink to the script, inside the /usr/libexec/sm.bin directory:
# ln -s /usr/local/bin/SendToMailing /usr/libexec/sm.bin/SendToMailing # ls -l /usr/libexec/sm.bin/ total 0 lrwxr-xr-x 1 root wheel 28 Oct 29 20:48 SendToMailing -> /usr/local/bin/SendToMailing #
Now it should work, and your little mailing list should be working and if you check the /var/log/maillog log, you should notice something like:
Oct 30 10:58:40 tyr sm-mta: l9U9wcln007289: alias <email@example.com> => "|/usr/local/bin/SendToMailing" Oct 30 10:58:48 tyr sm-mta: l9U9wcln007289: to="|/usr/local/bin/SendToMailing", ctladdr=<firstname.lastname@example.org> (1/0), delay=00:00:09, xdelay=00:00:08, mailer=prog, pri=31727, dsn=2.0.0, stat=Sent
when a new email for email@example.com arrives.