2 """An RFC 2821 smtp proxy.
4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
10 This program generally tries to setuid `nobody', unless this flag is
11 set. The setuid call will fail if this program is not run as root (in
12 which case, use this flag).
16 Print the version number and exit.
20 Use `classname' as the concrete SMTP proxy class. Uses `SMTPProxy' by
25 Turn on debugging prints.
29 Print this message and exit.
31 Version: %(__version__)s
33 If localhost is not given then `localhost' is used, and if localport is not
34 given then 8025 is used. If remotehost is not given then `localhost' is used,
35 and if remoteport is not given, then 25 is used.
82 __all__ = [
"SMTPServer",
"DebuggingServer",
"PureProxy",
"MailmanProxy"]
85 __version__ =
'Python SMTP proxy version 0.2'
101 print >> sys.stderr, __doc__ % globals()
103 print >> sys.stderr, msg
124 self.
__peer = conn.getpeername()
125 print >> DEBUGSTREAM,
'Peer:',
repr(self.
__peer)
126 self.
push(
'220 %s %s' % (self.
__fqdn, __version__))
135 self.__line.append(data)
139 line = EMPTYSTRING.join(self.
__line)
140 print >> DEBUGSTREAM,
'Data:',
repr(line)
144 self.
push(
'500 Error: bad syntax')
149 command = line.upper()
152 command = line[:i].
upper()
153 arg = line[i+1:].
strip()
154 method = getattr(self,
'smtp_' + command,
None)
156 self.
push(
'502 Error: command "%s" not implemented' % command)
162 self.
push(
'451 Internal confusion')
167 for text
in line.split(
'\r\n'):
168 if text
and text[0] ==
'.':
169 data.append(text[1:])
172 self.
__data = NEWLINE.join(data)
173 status = self.__server.process_message(self.
__peer,
189 self.
push(
'501 Syntax: HELO hostname')
192 self.
push(
'503 Duplicate HELO/EHLO')
199 self.
push(
'501 Syntax: NOOP')
209 def __getaddr(self, keyword, arg):
211 keylen = len(keyword)
212 if arg[:keylen].
upper() == keyword:
213 address = arg[keylen:].
strip()
216 elif address[0] ==
'<' and address[-1] ==
'>' and address !=
'<>':
219 address = address[1:-1]
223 print >> DEBUGSTREAM,
'===> MAIL', arg
226 self.
push(
'501 Syntax: MAIL FROM:<address>')
229 self.
push(
'503 Error: nested MAIL command')
232 print >> DEBUGSTREAM,
'sender:', self.
__mailfrom
236 print >> DEBUGSTREAM,
'===> RCPT', arg
238 self.
push(
'503 Error: need MAIL command')
242 self.
push(
'501 Syntax: RCPT TO: <address>')
244 if address.lower().startswith(
'stimpy'):
245 self.
push(
'503 You suck %s' % address)
247 self.__rcpttos.append(address)
248 print >> DEBUGSTREAM,
'recips:', self.
__rcpttos
253 self.
push(
'501 Syntax: RSET')
264 self.
push(
'503 Error: need RCPT command')
267 self.
push(
'501 Syntax: DATA')
271 self.
push(
'354 End data with <CR><LF>.<CR><LF>')
282 self.set_reuse_addr()
285 print >> DEBUGSTREAM, \
286 '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
287 self.__class__.__name__, time.ctime(time.time()),
288 localaddr, remoteaddr)
291 conn, addr = self.accept()
292 print >> DEBUGSTREAM,
'Incoming connection from %s' %
repr(addr)
297 """Override this abstract method to handle messages from the client.
299 peer is a tuple containing (ipaddr, port) of the client that made the
300 socket connection to our smtp port.
302 mailfrom is the raw address the client claims the message is coming
305 rcpttos is a list of raw addresses the client wishes to deliver the
308 data is a string containing the entire full text of the message,
309 headers (if supplied) and all. It has been `de-transparencied'
310 according to RFC 821, Section 4.5.2. In other words, a line
311 containing a `.' followed by other text has had the leading dot
314 This function should return None, for a normal `250 Ok' response;
315 otherwise it returns the desired response string in RFC 821 format.
318 raise NotImplementedError
326 lines = data.split(
'\n')
327 print '---------- MESSAGE FOLLOWS ----------'
330 if inheaders
and not line:
331 print 'X-Peer:', peer[0]
334 print '------------ END MESSAGE ------------'
340 lines = data.split(
'\n')
347 lines.insert(i,
'X-Peer: %s' % peer[0])
348 data = NEWLINE.join(lines)
349 refused = self.
_deliver(mailfrom, rcpttos, data)
351 print >> DEBUGSTREAM,
'we got some refusals'
353 def _deliver(self, mailfrom, rcpttos, data):
360 refused = s.sendmail(mailfrom, rcpttos, data)
364 print >> DEBUGSTREAM,
'got SMTPRecipientsRefused'
365 refused = e.recipients
367 print >> DEBUGSTREAM,
'got', e.__class__
371 errcode = getattr(e,
'smtp_code', -1)
372 errmsg = getattr(e,
'smtp_error',
'ignore')
374 refused[r] = (errcode, errmsg)
381 from cStringIO
import StringIO
382 from Mailman
import Utils
383 from Mailman
import Message
384 from Mailman
import MailList
390 local = rcpt.lower().
split(
'@')[0]
398 parts = local.split(
'-')
406 if not Utils.list_exists(listname)
or command
not in (
407 '',
'admin',
'owner',
'request',
'join',
'leave'):
409 listnames.append((rcpt, listname, command))
413 for rcpt, listname, command
in listnames:
416 print >> DEBUGSTREAM,
'forwarding recips:',
' '.
join(rcpttos)
418 refused = self.
_deliver(mailfrom, rcpttos, data)
420 print >> DEBUGSTREAM,
'we got refusals'
424 msg = Message.Message(s)
428 if not msg.getheader(
'from'):
429 msg[
'From'] = mailfrom
430 if not msg.getheader(
'date'):
431 msg[
'Date'] = time.ctime(time.time())
432 for rcpt, listname, command
in listnames:
433 print >> DEBUGSTREAM,
'sending message to', rcpt
434 mlist = mlists.get(listname)
436 mlist = MailList.MailList(listname, lock=0)
437 mlists[listname] = mlist
441 msg.Enqueue(mlist, tolist=1)
442 elif command ==
'admin':
443 msg.Enqueue(mlist, toadmin=1)
444 elif command ==
'owner':
445 msg.Enqueue(mlist, toowner=1)
446 elif command ==
'request':
447 msg.Enqueue(mlist, torequest=1)
448 elif command
in (
'join',
'leave'):
450 if command ==
'join':
451 msg[
'Subject'] =
'subscribe'
453 msg[
'Subject'] =
'unsubscribe'
454 msg.Enqueue(mlist, torequest=1)
460 classname =
'PureProxy'
468 sys.argv[1:],
'nVhc:d',
469 [
'class=',
'nosetuid',
'version',
'help',
'debug'])
470 except getopt.error, e:
474 for opt, arg
in opts:
475 if opt
in (
'-h',
'--help'):
477 elif opt
in (
'-V',
'--version'):
478 print >> sys.stderr, __version__
480 elif opt
in (
'-n',
'--nosetuid'):
482 elif opt
in (
'-c',
'--class'):
483 options.classname = arg
484 elif opt
in (
'-d',
'--debug'):
485 DEBUGSTREAM = sys.stderr
489 localspec =
'localhost:8025'
490 remotespec =
'localhost:25'
493 remotespec =
'localhost:25'
498 usage(1,
'Invalid arguments: %s' % COMMASPACE.join(args))
501 i = localspec.find(
':')
503 usage(1,
'Bad local spec: %s' % localspec)
504 options.localhost = localspec[:i]
506 options.localport = int(localspec[i+1:])
508 usage(1,
'Bad local port: %s' % localspec)
509 i = remotespec.find(
':')
511 usage(1,
'Bad remote spec: %s' % remotespec)
512 options.remotehost = remotespec[:i]
514 options.remoteport = int(remotespec[i+1:])
516 usage(1,
'Bad remote port: %s' % remotespec)
521 if __name__ ==
'__main__':
528 print >> sys.stderr, \
529 'Cannot import module "pwd"; try running with -n option.'
531 nobody = pwd.getpwnam(
'nobody')[2]
535 if e.errno != errno.EPERM:
raise
536 print >> sys.stderr, \
537 'Cannot setuid "nobody"; try running with -n option.'
540 class_ = getattr(__main__, options.classname)
541 proxy =
class_((options.localhost, options.localport),
542 (options.remotehost, options.remoteport))
545 except KeyboardInterrupt: