Braindead mailing list

POP3-based mailing list

This is a simple python script that can be used to implement a simple mailing list. All it does is retrieve all messages from a mailbox, process subscriptions and unsubscriptions, forward remaining messages to subscribed users and delete the messages from the mailbox. To actually make the mailing list working, you have to run this periodically – for example every hour.

import poplib
import smtplib

class PopMailList:

    members_file = 'members.list'

    def __init__(self, address, password):
        self.address = address
        self.user, self.host = address.split('@', 1)
        self.password = password
        try:
            self.members = file(self.members_file).readlines()
        except IOError:
            self.members = []

    def process_all_messages(self):
        pop = poplib.POP3(self.host)
        pop.user(self.user)
        pop.pass_(self.password)
        status, messages, size = pop.list()
        number = 0
        for info in messages:
            number, size = map(int, info.split(' ', 1))
            status, msg, size = pop.retr(number)
            self.process_message(msg)
            pop.dele(number)    # we delete the processed messages
        pop.quit()
        print "%d messages processed" % number

    def process_message(self, msg):
        fromaddr = ''
        toaddr = ''
        subject = ''
        for line in msg:
            if line.startswith('Subject:'):
                subject = line.split(':', 1)[1].strip()
            elif line.startswith('From:'):
                fromaddr = line.split(':', 1)[1]
                fromaddr = fromaddr[fromaddr.find('<')+1:fromaddr.find('>')].strip()
            elif line.startswith('To:'):
                toaddr = line.split(':', 1)[1]
                toaddr = toaddr[toaddr.find('<')+1:toaddr.find('>')].strip()
        if toaddr != self.address:
            return # skip any messages not directed to the list
        if fromaddr not in self.members:
            if subject.startswith('subscribe'):
                print "subscribing %s" % fromaddr
                self.subscribe_member(fromaddr)
            else:
                print "rejecting %s" % fromaddr
                self.reject_message(fromaddr)
        else:
            if subject.startswith('unsubscribe'):
                print "unsubscribing %s" % fromaddr
                self.unsubscribe_member(fromaddr)
            else:
                print "forwarding %s" % fromaddr
                self.forward_message(msg)

    def subscribe_member(self, address):
        self.members.append(address)
        self.save_members()
        self.send_message(address,
            ["", "Welcome to the %s mailing list!" % self.address], "Welcome")

    def unsubscribe_member(self, address):
        self.members.remove(address)
        self.save_members()
        self.send_message(address,
            ["", "You unsubscribed from %s." % self.address], "Good bye")

    def save_members(self):
        file(self.members_file, 'w').writelines(self.members)

    def reject_message(self, address):
        self.send_message(address,
["", "You need to subscribe to %s by sending" % self.address,
"empty message with 'subscribe' in the subject to be able to post here."],
"Rejected")

    def forward_message(self, msg):
        new_msg = []
        for line in msg:
            if (line.startswith('To:') or
                line.startswith('Received:') or
                line.startswith('Delivered-To:') or
                line.startswith('X-Original-To:') or
                line.startswith('Return-path:')):
                pass
            else:
                new_msg.append(line)
        for member in self.members:
            print 'sending to %s' % member
            self.send_message(member, new_msg)

    def send_message(self, address, msg, subject=None):
        if subject:
            msg = ["Subject: %s" % subject] + msg
        msg.insert(0, "To: %s" % address)
        msg.insert(0, "Reply-To: %s" % self.address)
        server = smtplib.SMTP(self.host, 25)
        server.login(self.user, self.password)
        server.sendmail(self.address, address, '\r\n'.join(msg))
        server.quit()
 
ml = PopMailList('mailbox@mailhost', 'password')
ml.process_all_messages()

The member list is kept in a “members.list” file (we don’t do any locking, but pop3 lock the mailbox for us, so only one instance of the script can run at a time).

I assumed that the addresses of the mailbox, the pop3 server and the smtp server are all the same, and that the smtp server requires authentication.

The subscription and unsubscription logic is based on the message’s subjects, but could be somewhat improved too (e.g. don’t forward the ‘subscribe’ messages if the member is already subscribed).

The sending of messages is done in a highly suboptimal way – we log in to the server and authenticate for every single message – this can be improved. A little more effective version follows:

import poplib
import smtplib

class PopMailList:

    members_file = 'members.list'

    def __init__(self, address, password):
        self.address = address
        self.user, self.host = address.split('@', 1)
        self.password = password
        # read member list from file
        try:
            self.members = file(self.members_file).readlines()
        except IOError:
            self.members = []
        # open connection to pop3 server
        self.pop3 = poplib.POP3(self.host)
        self.pop3.user(self.user)
        self.pop3.pass_(self.password)
        # open connection to smtp server
        self.smtp = smtplib.SMTP(self.host, 25)
        self.smtp.login(self.user, self.password)

    def close(self):
        self.pop3.quit()
        self.smtp.quit()

    def process_all_messages(self):
        status, messages, size = self.pop3.list()
        number = 0
        for info in messages:
            number, size = map(int, info.split(' ', 1))
            status, msg, size = self.pop3.retr(number)
            self.process_message(msg)
            self.pop3.dele(number)    # we delete the processed messages
        print "%d messages processed" % number

    def process_message(self, msg):
        # read the headers
        fromaddr = ''
        toaddr = ''
        subject = ''
        for line in msg:
            if line.startswith('Subject:'):
                subject = line.split(':', 1)[1].strip()
            elif line.startswith('From:'):
                fromaddr = line.split(':', 1)[1]
                fromaddr = fromaddr[fromaddr.find('<')+1:fromaddr.find('>')]
            elif line.startswith('To:'):
                toaddr = line.split(':', 1)[1]
                toaddr = toaddr[toaddr.find('<')+1:toaddr.find('>')]
            elif line == "":
                break;
        # decide what to do
        if fromaddr not in self.get_members():
            if subject.startswith('subscribe'):
                print "subscribing %s" % fromaddr
                self.subscribe_member(fromaddr)
            else:
                print "rejecting %s" % fromaddr
                self.reject_message(fromaddr)
        else:
            if subject.startswith('unsubscribe'):
                print "unsubscribing %s" % fromaddr
                self.unsubscribe_member(fromaddr)
            else:
                print "forwarding %s" % fromaddr
                self.forward_message(msg)

    def subscribe_member(self, address):
        self.get_members()
        self.members.append(address)
        self.save_members()
        self.send_message(address,
["", "Welcome to the %s mailing list!" % self.address], subject="Welcome")

    def unsubscribe_member(self, address):
        self.get_members()
        self.members.remove(address)
        self.save_members()
        self.send_message(address,
["", "You unsubscribed from %s." % self.address], subject="Good bye")

    def save_members(self):
        file(self.members_file, 'w').writelines(self.members)

    def reject_message(self, address):
        self.send_message(address,
["", "You need to subscribe to %s by sending" % self.address,
"empty message with 'subscribe' in the subject",
"to be able to post here."], subject="Rejected")

    def forward_message(self, msg):
        new_msg = []
        for line in msg:
            if (line.startswith('To:') or
                line.startswith('Received:') or
                line.startswith('Delivered-To:') or
                line.startswith('X-Original-To:') or
                line.startswith('Return-path:')):
                pass
            else:
                new_msg.append(line)
        for member in self.get_members():
            print 'sending to %s' % member
            self.send_message(member, new_msg)

    def send_message(self, address, msg, subject=None):
        if subject is not None:
            msg.inser(0, "Subject: %s" % subject)
        head = "To: %s\r\nReply-To: %s\r\n" % (address, self.address)
        self.smtp.sendmail(self.address, address, head + '\r\n'.join(msg))
 
ml = PopMailList('mailbox@mailhost', 'password')
ml.process_all_messages()
ml.close()

Previous approach

This is an extremely simplified mailing list server. It’s hardly useful for anything but a proof of concept. Don’t use even at your own risk ;)

import smtplib
import smtpd
import asyncore

class MailListServer(smtpd.SMTPServer):
    """
    Implements a braindead mailing list server.
    Warning! This code is for educational purposes only, don't use it
    for running real mailing lists! It's very insecure!
    """

    members = []

    def process_message(self, peer, mailfrom, rcpttos, data):
        """Receive messages."""

        # Automatically subscribe anyone who posts a message
        if mailfrom not in self.members:
            members.append(mailfrom)
        
        # Forward the messages to all members
        for member in self.members:
            self.send_message([member], data)

    def send_message(self, rcpttos, data):
        """Send a message using a real smtp server somewhere."""

        fromaddr = "your@mail.address"
        msg = "From: %s\r\nTo: %s\r\n\r\n%s" % (
                fromaddr,
                ", ".join(rcpttos),
                data
            )
        relay = smtplib.SMTP('your.mail.server', 25)
        relay.sendmail(fromaddr, toaddrs, msg)
        relay.quit()

if __name__=='__main__':
    server = MailListServer(('', 25), None)
    try:
        asyncore.loop(timeout=2)
    except KeyboardInterrupt:
        server.close()