| 1 | #!/usr/bin/python |
|---|
| 2 | #vim:fileencoding=utf-8 |
|---|
| 3 | |
|---|
| 4 | ''' |
|---|
| 5 | ml.py -- simple mailing list to just distribute messages in a group. |
|---|
| 6 | |
|---|
| 7 | Copyright (c) 2007,2010 Shigeru KANEMOTO |
|---|
| 8 | All rights reserved. |
|---|
| 9 | ''' |
|---|
| 10 | |
|---|
| 11 | LIST_ADDRESS = 'list@example.com' |
|---|
| 12 | LIST_SUBSCRIBERS = { |
|---|
| 13 | # 'email address (lower case)': u'real name', |
|---|
| 14 | 'somebody@example.com': u'なまえ', |
|---|
| 15 | } |
|---|
| 16 | |
|---|
| 17 | import sys |
|---|
| 18 | import re |
|---|
| 19 | import email.FeedParser |
|---|
| 20 | import email.Header |
|---|
| 21 | import email.Utils |
|---|
| 22 | import smtplib |
|---|
| 23 | |
|---|
| 24 | BUFFER_SIZE = 1024 |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | class MLError(RuntimeError): |
|---|
| 28 | pass |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | def message_from_fileobj(fileobj): |
|---|
| 32 | parser = email.FeedParser.FeedParser() |
|---|
| 33 | while True: |
|---|
| 34 | buffer = fileobj.read(BUFFER_SIZE) |
|---|
| 35 | if not buffer: |
|---|
| 36 | return parser.close() |
|---|
| 37 | parser.feed(buffer) |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | def alter_message_for_forwarding(message): |
|---|
| 41 | # Check the destination |
|---|
| 42 | recipients = message.get_all('to', []) |
|---|
| 43 | recipients += message.get_all('cc', []) |
|---|
| 44 | recipients += message.get_all('resent-to', []) |
|---|
| 45 | recipients += message.get_all('resent-cc', []) |
|---|
| 46 | recipients = email.Utils.getaddresses(recipients) |
|---|
| 47 | for (realname, address) in recipients: |
|---|
| 48 | if address.lower() == LIST_ADDRESS: |
|---|
| 49 | break |
|---|
| 50 | else: |
|---|
| 51 | raise MLError, 'Not sent to this list.' |
|---|
| 52 | |
|---|
| 53 | # Get nickname of the sender |
|---|
| 54 | sender = message['from'] |
|---|
| 55 | if not sender: |
|---|
| 56 | raise MLError, 'No "from" header found.' |
|---|
| 57 | (realname, sender) = email.Utils.parseaddr(sender) |
|---|
| 58 | if not sender: |
|---|
| 59 | raise MLError, '"from" header not recognized.' |
|---|
| 60 | sender = sender.lower() |
|---|
| 61 | try: |
|---|
| 62 | nickname = LIST_SUBSCRIBERS[sender] |
|---|
| 63 | except KeyError: |
|---|
| 64 | raise MLError, 'Sender "%s" not allowed.' % sender |
|---|
| 65 | |
|---|
| 66 | # Get the subject |
|---|
| 67 | subject = message.get('subject', '') |
|---|
| 68 | subject = email.Header.decode_header(subject) |
|---|
| 69 | (s, charset) = subject[0] |
|---|
| 70 | s = re.sub(r'^(Re|RE|re)(\d*|\[\d+\])[:>]\s*(Bcc:\s*|)', '', s, 1) |
|---|
| 71 | subject[0] = (s, charset) |
|---|
| 72 | subject = email.Header.make_header(subject) |
|---|
| 73 | |
|---|
| 74 | # Prepare the new message |
|---|
| 75 | del message['to'] |
|---|
| 76 | del message['cc'] |
|---|
| 77 | del message['resent-to'] |
|---|
| 78 | del message['resent-cc'] |
|---|
| 79 | del message['from'] |
|---|
| 80 | del message['reply-to'] |
|---|
| 81 | del message['subject'] |
|---|
| 82 | message['To'] = LIST_ADDRESS |
|---|
| 83 | message['From'] = LIST_ADDRESS |
|---|
| 84 | message['Subject'] = subject |
|---|
| 85 | |
|---|
| 86 | def insert_nickname(m): |
|---|
| 87 | if m.get_content_maintype() != 'text': |
|---|
| 88 | return |
|---|
| 89 | charset = m.get_content_charset().lower() |
|---|
| 90 | body = m.get_payload(i=None, decode=True) |
|---|
| 91 | |
|---|
| 92 | # Decode |
|---|
| 93 | if charset == 'iso-2022-jp': |
|---|
| 94 | #XXX 「㍑」対応 |
|---|
| 95 | body = body.replace('\x1b$B', '\x1b$(Q').decode('iso-2022-jp-2004') |
|---|
| 96 | else: |
|---|
| 97 | body = body.decode(charset) |
|---|
| 98 | |
|---|
| 99 | # Insert |
|---|
| 100 | if m.get_content_subtype() == 'html': |
|---|
| 101 | body = re.sub(r'^(.*<(body|BODY).*>|)', r'\1[%s]<br />' % nickname, body, 1) |
|---|
| 102 | else: |
|---|
| 103 | body = ('[%s]\r\n' % nickname) + body |
|---|
| 104 | |
|---|
| 105 | # Encode |
|---|
| 106 | #XXX 「㍑」対応 |
|---|
| 107 | body = body.encode('iso-2022-jp-2004').replace('\033$(Q', '\033$B') |
|---|
| 108 | del m['content-transfer-encoding'] |
|---|
| 109 | m.set_payload(body, 'iso-2022-jp') |
|---|
| 110 | |
|---|
| 111 | if message.is_multipart(): |
|---|
| 112 | for m in message.get_payload(): |
|---|
| 113 | insert_nickname(m) |
|---|
| 114 | else: |
|---|
| 115 | insert_nickname(message) |
|---|
| 116 | |
|---|
| 117 | return sender |
|---|
| 118 | |
|---|
| 119 | |
|---|
| 120 | def deliver_message(sender, message): |
|---|
| 121 | recipients = LIST_SUBSCRIBERS.keys() |
|---|
| 122 | recipients.remove(sender) |
|---|
| 123 | smtp = smtplib.SMTP('localhost') |
|---|
| 124 | smtp.sendmail(LIST_ADDRESS, recipients, message.as_string()) |
|---|
| 125 | smtp.close() |
|---|
| 126 | |
|---|
| 127 | |
|---|
| 128 | if __name__ == '__main__': |
|---|
| 129 | try: |
|---|
| 130 | message = message_from_fileobj(sys.stdin) |
|---|
| 131 | sender = alter_message_for_forwarding(message) |
|---|
| 132 | deliver_message(sender, message) |
|---|
| 133 | except MLError, e: |
|---|
| 134 | print >>sys.stderr, str(e) |
|---|
| 135 | # Courier suspends delivery to same address for a while after exiting |
|---|
| 136 | # with exit code except zero. |
|---|
| 137 | sys.exit(0) |
|---|