Simon Volpert remailer / 03b53a0
Migrated to git; Previous versioning info unavailable. Simon Volpert 9 years ago
11 changed file(s) with 478 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/bin/python
1
2 # Copyright 2008 Lenny Domnitser <http://domnit.org/>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or (at
7 # your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 __all__ = 'clarify',
18 __author__ = 'Lenny Domnitser'
19 __version__ = '0.1'
20
21 import email
22 import re
23
24 TEMPLATE = '''-----BEGIN PGP SIGNED MESSAGE-----
25 Hash: %(hashname)s
26 NotDashEscaped: You need GnuPG to verify this message
27
28 %(text)s%(sig)s'''
29
30
31 def _clarify(message, messagetext):
32 if message.get_content_type() == 'multipart/signed':
33 if message.get_param('protocol') == 'application/pgp-signature':
34 hashname = message.get_param('micalg').upper()
35 assert hashname.startswith('PGP-')
36 hashname = hashname.replace('PGP-', '', 1)
37 textmess, sigmess = message.get_payload()
38 assert sigmess.get_content_type() == 'application/pgp-signature'
39 #text = textmess.as_string() - not byte-for-byte accurate
40 text = messagetext.split('\n--%s\n' % message.get_boundary(), 2)[1]
41 sig = sigmess.get_payload()
42 assert isinstance(sig, str)
43 # Setting content-type to application/octet instead of text/plain
44 # to maintain CRLF endings. Using replace_header instead of
45 # set_type because replace_header clears parameters.
46 message.replace_header('Content-Type', 'application/octet')
47 clearsign = TEMPLATE % locals()
48 clearsign = clearsign.replace(
49 '\r\n', '\n').replace('\r', '\n').replace('\n', '\r\n')
50 message.set_payload(clearsign)
51 elif message.is_multipart():
52 for message in message.get_payload():
53 _clarify(message, messagetext)
54
55
56 def clarify(messagetext):
57 '''given a string containing a MIME message, returns a string
58 where PGP/MIME messages are replaced with clearsigned messages.'''
59
60 message = email.message_from_string(messagetext)
61 _clarify(message, messagetext)
62 return message.as_string()
63
64
65 if __name__ == '__main__':
66 import sys
67 sys.stdout.write(clarify(sys.stdin.read()))
0 backend=sendmail OR smtp OR dummy
1 server=SMTP-SERVER
2 login=SMTP-LOGIN
3 passwd=SMTP-PASSWORD
4 allowed_senders=COMMA_SEPARATED_EMAIL_LIST
5 list_owner=FROM-STRING
6 gnupg_file=/home/USERNAME/.gnupg/pubring.gpg
0 #!/usr/bin/python -B
1 # Import the public GPG key for the persons allowed to post into the keyring on the server
2 # Add the From: strings for the persons allowed to post to the allowed sender list
3 # Create an exim filter that pipes emails coming to your list into this script
4
5 # -----===== CONFIGURATION =====-----
6 files = {
7 'headers': '%s/headers.txt', # Email headers to be included in sent messages
8 'subscribers': '%s/subscribers.txt', # List of newsletter subscribers
9 'welcome': '%s/welcome.txt', # The message sent when someone subscribes
10 'farewell': '%s/farewell.txt', # The message sent when someone unsubscribes
11 'confirmation': '%s/confirm.txt', # The message sent when someone requests subscription
12 'details_missing': '%s/details_missing.txt', # The message sent when a details request is made on a non-existent email
13 'details_verified': '%s/details_verified.txt', # The message sent when a details request is made on a verified email
14 'details_pending': '%s/details_pending.txt', # The message sent when a details request is made on an email pending verification
15 'details_bounced': '%s/details_bounced.txt' # The message sent when a details request is made on a disabled email
16 }
17 # See also remailer.cfg
18
19 import sys
20 import os
21 import email
22 import time
23 import subprocess
24 import string
25 import random
26 import copy
27 import logging
28
29 # Some web hosts are not very diligent in keeping up-to-date
30 try:
31 from email.mime.text import MIMEText # Python 2.7+
32 except ImportError:
33 from email.MIMEText import MIMEText # Python 2.4
34
35 try:
36 from email.mime.multipart import MIMEMultipart # Python 2.7+
37 except ImportError:
38 from email.MIMEMultipart import MIMEMultipart # Python 2.4
39
40 try:
41 from email.utils import parseaddr # Python 2.7+
42 except ImportError:
43 from email.Utils import parseaddr # Python 2.4
44
45 # Those are local scripts put in the same directory
46 import clearmime
47
48 # Configuration file parsing and validation
49 # The simplicity of this implementation does not warrant employing a ConfigParser
50 conf = {}
51 try:
52 c = open('remailer.cfg', 'r')
53 for l in c.readlines():
54 k, v = l.strip().split('=')[0], l.strip().split('=')[1:]
55 if type(v) == type([]):
56 v = '='.join(v)
57 conf[k] = v
58 c.close()
59 except:
60 panic('No configuration file found')
61 for s in ['backend', 'server', 'login', 'passwd', 'allowed_senders', 'list_owner', 'gnupg_file']:
62 if s not in conf:
63 panic('`%s` key is missing from configuration file' % s)
64
65 # Abort the execution leaving sending the list owner a backtrace
66 def panic(error):
67 logging.error('Panicking: %s' % error)
68 debug_file = '%s.eml' % time.time()
69 df = open(debug_file, 'w')
70 df.write(raw_email)
71 df.close()
72 logging.error('Offending message saved to "%s"' % debug_file)
73 sys.exit()
74
75 # Send one or more pre-constructed emails
76 def sendmail(message_list):
77 if type(message_list) != type([]):
78 message_list = [message_list]
79 sent_count = 0
80 if conf['backend'] == 'smtp':
81 import smtplib
82 try:
83 logging.info('Connecting to SMTP server')
84 server = smtplib.SMTP(conf['server'])
85 server.login(conf['login'], conf['passwd'])
86 except:
87 panic('SMTP failed: %s' % sys.exc_info()[0])
88 for message in message_list:
89 try:
90 server.sendmail(message['From'], message['To'], message.as_string())
91 except:
92 logging.error('Sending to %s failed: %s' % (message['To'], sys.exc_info()[0]))
93 else:
94 sent_count += 1
95 #logging.debug('Sent to %s' % message['To'])
96 server.quit()
97 elif conf['backend'] == 'sendmail':
98 for message in message_list:
99 try:
100 server = subprocess.Popen(['/usr/sbin/sendmail','-i', '-t'], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
101 _, stderr = server.communicate(message.as_string())
102 if stderr != '':
103 logging.error('Sending to %s failed' % message['To'])
104 logging.error(stderr)
105 except:
106 logging.error('Sendmail failed: %s' % sys.exc_info()[0])
107 else:
108 sent_count += 1
109 elif conf['backend'] == 'dummy':
110 logging.info('Pretending to send messages')
111 else:
112 panic('Back-end for sending mail is not configured')
113 if sent_count == 0:
114 panic('No valid recipients for %s' % list_name)
115 else:
116 logging.info('%s message(s) sent' % sent_count)
117
118
119 # Extract a relevant record from the subscriber database
120 def find_subscriber(subscriber):
121 try:
122 rfile = open(files['subscribers'])
123 except:
124 panic('Could not load subscriber list for %s: %s' % (list_name, sys.exc_info()[0]))
125 else:
126 for raw_recipient in rfile:
127 if raw_recipient == '\n':
128 continue
129 recipient = raw_recipient.strip().split(' ')
130 r_name, r_email = parseaddr(recipient[0])
131 if r_email == subscriber:
132 rfile.close()
133 return recipient
134 return None
135
136 # Extract the message sender
137 def get_sender(message):
138 address = None
139 for header in ['From', 'From_', 'Reply-To']:
140 if not message[header] == None:
141 _, address = parseaddr(message[header])
142 return address
143 panic('Could not find the sender address')
144
145 # Load a file as message
146 def message_from_file(filename, args=()):
147 try:
148 wfile = open(filename)
149 except:
150 panic('Could not load %s: %s' % (filename, sys.exc_info()[0]))
151 else:
152 message = MIMEText(wfile.read() % args)
153 wfile.close()
154 message['From'] = '%s-request@simonvolpert.com' % list_name
155 message['Auto-Submitted'] = 'auto-responded'
156 return message
157
158 # Send a report about a subscriber
159 def report(address):
160 subscriber = find_subscriber(address)
161 if subscriber == None:
162 message = message_from_file(files['details_missing'] , address)
163 message['Subject'] = 'Email not found'
164 else:
165 if subscriber[3] == 'verified':
166 message = message_from_file(files['details_verified'], (subscriber[0], subscriber[1], subscriber[2], subscriber[4]))
167 message['Subject'] = 'Subscription details'
168 elif subscriber[3] == 'bounce':
169 message = message_from_file(files['details_bounced'], (subscriber[0], subscriber[1], subscriber[2]))
170 message['Subject'] = 'Subscription disabled'
171 else:
172 message = message_from_file(files['details_pending'], (subscriber[0], subscriber[1], subscriber[2])) # TODO make it double as a re-subscription, maybe
173 message['Subject'] = 'Verification pending'
174 message['To'] = address
175 sendmail(insert_headers(message))
176 sys.exit()
177
178 # Insert mailing-list headers into the message
179 def insert_headers(message):
180 try:
181 hfile = open(files['headers'])
182 except:
183 panic('Could not load header list for %s: %s' % (list_name, sys.exc_info()[0]))
184 else:
185 for line in hfile:
186 header, header_text = line.strip().split('|')
187 message[header] = header_text
188 hfile.close()
189 return message
190
191
192 # -----===== EMAIL PROCESSING =====-----
193 os.chdir(sys.path[0])
194
195 logging.basicConfig(filename='remailer.log', level=logging.DEBUG, format='%(asctime)s %(message)s')
196
197 # This script works on emails piped into it
198 raw_email = sys.stdin.read()
199 message = email.message_from_string(raw_email)
200
201 logging.info('Incoming message from %s (%s bytes)' % (message['From'], len(raw_email)))
202
203 # Figure out the list name from the message headers
204 # Recipients checked in reverse order of directness
205 list_name = None
206 for header in ['To', 'CC', 'BCC']:
207 if not message[header] == None:
208 _, list_name = parseaddr(message[header])
209 if list_name == None:
210 panic('Could not figure out the list name')
211 else:
212 list_name = list_name.split('@')[0]
213 if list_name.endswith('-request'):
214 list_name = list_name.split('-')[0]
215 request = True
216 logging.info('REQUEST mode on "%s"' % list_name)
217 else:
218 request = False
219 logging.info('POST mode on "%s"' % list_name)
220 for item in files:
221 files[item] = files[item] % list_name
222 # Delete original message recipients
223 for header in ['To', 'CC', 'BCC']:
224 if not message[header] == None:
225 del message[header]
226
227 # -----===== SUBSCRIPTION MANAGEMENT =====-----
228 if request:
229 # If it's an auto-response, discard it
230 if message['Auto-Submitted'] != None:
231 panic('Message is an auto-response')
232 # -----===== SUBSCRIBE =====-----
233 logging.info('Subject: %s' % message['Subject'])
234 if message['Subject'].lower().startswith('subscribe'):
235 message_from = get_sender(message)
236 subscriber = find_subscriber(message_from)
237 if not subscriber == None:
238 if subscriber[3] == 'verified':
239 report(subscriber[0])
240 else:
241 # Comment out for dry-run
242 subprocess.call(['sed', '-i', '-e', '/%s/d' % subscriber[0], files['subscribers']])
243 logging.info('Subscriber info UPDATED for %s' % subscriber[0])
244 now = time.asctime(time.gmtime())
245 medium = 'email'
246 subscriber = [message_from, now, medium, '']
247 subscriber[3] = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(8))
248 # Comment out for dry-run
249 sfile = open(files['subscribers'], 'a')
250 sfile.write(' '.join(subscriber) + '\n')
251 sfile.close()
252 logging.info('Subscriber info WRITTEN for %s' % subscriber[0])
253 message = message_from_file(files['confirmation'], (message_from, now, medium))
254 message['To'] = subscriber[0]
255 message['Subject'] = 'Confirm your subscription - %s' % subscriber[3]
256 sendmail(insert_headers(message))
257 # -----===== UNSUBSCRIBE =====-----
258 elif message['Subject'].lower().startswith('unsubscribe'):
259 message_from = get_sender(message)
260 subscriber = find_subscriber(message_from)
261 if subscriber == None:
262 report(message_from)
263 else:
264 # Comment out for dry-run
265 subprocess.call(['sed', '-i', '-e', '/%s/d' % subscriber[0], files['subscribers']])
266 logging.info('Subscriber info REMOVED for %s' % subscriber[0])
267 message = message_from_file(files['farewell'], message_from)
268 message['To'] = subscriber[0]
269 message['Subject'] = 'Unsubscription successful'
270 sendmail(insert_headers(message))
271 # -----===== DETAILS =====-----
272 elif message['Subject'].lower().startswith('details') or message['Subject'].lower().startswith('status') or message['Subject'].lower().startswith('info'):
273 report(get_sender(message))
274 # -----===== CONFIRM =====-----
275 elif message['Subject'].lower().startswith('re: confirm '):
276 message_from = get_sender(message)
277 subscriber = find_subscriber(message_from)
278 if not subscriber == None:
279 if subscriber[3] == 'verified' or subscriber[3] == 'bounce':
280 report(subscriber[0])
281 confirm_string = message['Subject'].split(' ')[-1]
282 message_from = get_sender(message)
283 subscriber = find_subscriber(message_from)
284 if subscriber == None:
285 report(message_from)
286 else:
287 if confirm_string == subscriber[3]:
288 # Comment out for dry-run
289 subprocess.call(['sed', '-i', '-e', 's/%s.*/verified %s/' % (confirm_string, time.asctime(time.gmtime())), files['subscribers']])
290 logging.info('Subscriber info UPDATED for %s' % subscriber[0])
291 message = message_from_file(files['welcome'])
292 message['To'] = subscriber[0]
293 message['Subject'] = 'Confirmation successful'
294 sendmail(insert_headers(message))
295 else:
296 panic('Confirmation string does not match')
297 else:
298 panic('Unknown command')
299 # -----===== PUBLISHING =====-----
300 else:
301 # Bounce unknown senders # TODO
302 # if get_sender(message) not in conf.allowed_senders:
303 # panic('Unauthorized sender: %s' % message['From'])
304 if message['From'] != conf['list_owner']:
305 panic('Unauthorized sender: %s' % message['From'])
306
307 # Verify the message sender's GPG signature
308 clear_email = clearmime.clarify(raw_email)
309 server = subprocess.Popen(['gpgv', '--keyring', conf['gnupg_file']], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
310 _, stderr = server.communicate(clear_email)
311 if not ( 'gpgv: Good signature from "%s"' % conf['list_owner'] in stderr ): # TODO expand to all allowed-senders
312 logging.info(stderr)
313 panic('GPG Signature verification failed')
314 logging.info('Signature check successful')
315
316 message = insert_headers(message)
317 # Re-send the email to every verified subscriber from the subscriber list
318 # Format: email, subscription date, subscription medium, status, confirmation date (tab separated)
319 try:
320 rfile = open(files['subscribers'])
321 except:
322 panic('Could not load subscriber list for %s: %s' % (list_name, sys.exc_info()[0]))
323 message_list = []
324 for recipient in rfile:
325 if recipient == '\n':
326 continue
327 recipient = recipient.strip().split(' ')
328 if not recipient[3] == 'verified':
329 continue
330 del message['To']
331 message['To'] = recipient[0]
332 message_list.append(copy.copy(message))
333 rfile.close()
334 sendmail(message_list)
0 Good time-of-the-day, person.
1
2 Please confirm your subscription to the %%NAME%% newsletter by replying to this email (that is, pressing "Reply" and immediately pressing "Send").
3
4 To remind you, the subscription request was made for %s on %s via %s.
5
6 If you do not recall making such a request, you may ignore this email, with no consequences.
7
8 Regards,
9 The %%NAME%% newsletter subscription management script.
0 Good time-of-the-day, person.
1
2 According to my records, you (%s) subscribed to the %%NAME%% newsletter on %s via %s, however your subscription was disabled due to bounced emails.
3
4 If you would like to re-activate your subscription, please go through the subscription procedure again (that is, send an email with the subject "subscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>).
5
6 To permanently remove your email from my records, please send an email with the subject "unsubscribe" instead.
7
8 Regards,
9 The %%NAME%% newsletter subscription management script.
0 Good time-of-the-day, person.
1
2 You (%s) are not currently subscribed to the %%NAME%% newsletter.
3
4 If you would like to subscribe, please send an email with the subject "subscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>.
5
6 If you are subscribed from a different email than the one you are currently using, please be sure to unsubscribe that other one by sending an email with the subject "unsubscribe" from it. If you cannot access the subscribed email, please contact the list owner for manual unsubscription.
7
8 Regards,
9 The %%NAME%% newsletter subscription management script.
0 Good time-of-the-day, person.
1
2 According to my records, you (%s) subscribed to the %%NAME%% newsletter on %s via %s. A verification message was sent to your email, containing instructions for activating your subscription. If you didn't get it, you could try re-requesting it by going through the subscription procedure again (that is, sending an email with the subject "subscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>).
3
4 If you have changed your mind, and would like to permanently remove your email from my records, please send an email with the subject "unsubscribe" instead, or simply wait. Unconfirmed subscriptions time out eventually.
5
6 Regards,
7 The %%NAME%% newsletter subscription management script.
0 Good time-of-the-day, person.
1
2 According to my records, you (%s) subscribed to the %%NAME%% newsletter on %s via %s and subsequently confirmed your subscription on %s.
3
4 If you would like to unsubscribe, please send an email with the subject "unsubscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>. If you cannot access the subscribed email, please contact the list owner for manual unsubscription.
5
6 Regards,
7 The %%NAME%% newsletter subscription management script.
0 Good time-of-the-day, person.
1
2 As per your request, your email (%s) was unsubscribed from the %%NAME%% newsletter.
3
4 If at any point you would like to re-activate your subscription, you will need to go through the subscription procedure again (that is, to send an email with the subject "subscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>).
5
6 Regards,
7 The %%NAME%% newsletter subscription management script.
0 List-ID|Example ID <Example.DomainName.com>
1 List-Post|NO
2 List-Help|<mailto:%%LIST_ID%%-request@%%DOMAIN%%.com?subject=details>
3 List-Subscribe|<mailto:%%LIST_ID%%-request@%%DOMAIN%%.com?subject=subscribe>
4 List-Unsubscribe|<mailto:%%LIST_ID%%-request@%%DOMAIN%%.com?subject=unsubscribe>
5 List-Owner|<mailto:%%EMAIL%%>
0 Welcome to the %%NAME%% newsletter.
1
2 To unsubscribe from this mailing, please send a message with the subject "unsubscribe" to <%%LIST_ID%%-request@%%DOMAIN%%.com>.
3
4 For a reminder how and when you subscribed to the newsletter, send a message with the subject "details" instead.
5
6 Regards,
7 The %%NAME%% newsletter subscription management script.