]> git.proxmox.com Git - mirror_edk2.git/blob - AppPkg/Applications/Python/Python-2.7.2/Lib/smtpd.py
EmbeddedPkg: Extend NvVarStoreFormattedLib LIBRARY_CLASS
[mirror_edk2.git] / AppPkg / Applications / Python / Python-2.7.2 / Lib / smtpd.py
1 #! /usr/bin/env python
2 """An RFC 2821 smtp proxy.
3
4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5
6 Options:
7
8 --nosetuid
9 -n
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).
13
14 --version
15 -V
16 Print the version number and exit.
17
18 --class classname
19 -c classname
20 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
21 default.
22
23 --debug
24 -d
25 Turn on debugging prints.
26
27 --help
28 -h
29 Print this message and exit.
30
31 Version: %(__version__)s
32
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.
36 """
37
38 # Overview:
39 #
40 # This file implements the minimal SMTP protocol as defined in RFC 821. It
41 # has a hierarchy of classes which implement the backend functionality for the
42 # smtpd. A number of classes are provided:
43 #
44 # SMTPServer - the base class for the backend. Raises NotImplementedError
45 # if you try to use it.
46 #
47 # DebuggingServer - simply prints each message it receives on stdout.
48 #
49 # PureProxy - Proxies all messages to a real smtpd which does final
50 # delivery. One known problem with this class is that it doesn't handle
51 # SMTP errors from the backend server at all. This should be fixed
52 # (contributions are welcome!).
53 #
54 # MailmanProxy - An experimental hack to work with GNU Mailman
55 # <www.list.org>. Using this server as your real incoming smtpd, your
56 # mailhost will automatically recognize and accept mail destined to Mailman
57 # lists when those lists are created. Every message not destined for a list
58 # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
59 # are not handled correctly yet.
60 #
61 # Please note that this script requires Python 2.0
62 #
63 # Author: Barry Warsaw <barry@python.org>
64 #
65 # TODO:
66 #
67 # - support mailbox delivery
68 # - alias files
69 # - ESMTP
70 # - handle error codes from the backend smtpd
71
72 import sys
73 import os
74 import errno
75 import getopt
76 import time
77 import socket
78 import asyncore
79 import asynchat
80
81 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
82
83 program = sys.argv[0]
84 __version__ = 'Python SMTP proxy version 0.2'
85
86
87 class Devnull:
88 def write(self, msg): pass
89 def flush(self): pass
90
91
92 DEBUGSTREAM = Devnull()
93 NEWLINE = '\n'
94 EMPTYSTRING = ''
95 COMMASPACE = ', '
96
97
98 def usage(code, msg=''):
99 print >> sys.stderr, __doc__ % globals()
100 if msg:
101 print >> sys.stderr, msg
102 sys.exit(code)
103
104
105 class SMTPChannel(asynchat.async_chat):
106 COMMAND = 0
107 DATA = 1
108
109 def __init__(self, server, conn, addr):
110 asynchat.async_chat.__init__(self, conn)
111 self.__server = server
112 self.__conn = conn
113 self.__addr = addr
114 self.__line = []
115 self.__state = self.COMMAND
116 self.__greeting = 0
117 self.__mailfrom = None
118 self.__rcpttos = []
119 self.__data = ''
120 self.__fqdn = socket.getfqdn()
121 try:
122 self.__peer = conn.getpeername()
123 except socket.error, err:
124 # a race condition may occur if the other end is closing
125 # before we can get the peername
126 self.close()
127 if err[0] != errno.ENOTCONN:
128 raise
129 return
130 print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
131 self.push('220 %s %s' % (self.__fqdn, __version__))
132 self.set_terminator('\r\n')
133
134 # Overrides base class for convenience
135 def push(self, msg):
136 asynchat.async_chat.push(self, msg + '\r\n')
137
138 # Implementation of base class abstract method
139 def collect_incoming_data(self, data):
140 self.__line.append(data)
141
142 # Implementation of base class abstract method
143 def found_terminator(self):
144 line = EMPTYSTRING.join(self.__line)
145 print >> DEBUGSTREAM, 'Data:', repr(line)
146 self.__line = []
147 if self.__state == self.COMMAND:
148 if not line:
149 self.push('500 Error: bad syntax')
150 return
151 method = None
152 i = line.find(' ')
153 if i < 0:
154 command = line.upper()
155 arg = None
156 else:
157 command = line[:i].upper()
158 arg = line[i+1:].strip()
159 method = getattr(self, 'smtp_' + command, None)
160 if not method:
161 self.push('502 Error: command "%s" not implemented' % command)
162 return
163 method(arg)
164 return
165 else:
166 if self.__state != self.DATA:
167 self.push('451 Internal confusion')
168 return
169 # Remove extraneous carriage returns and de-transparency according
170 # to RFC 821, Section 4.5.2.
171 data = []
172 for text in line.split('\r\n'):
173 if text and text[0] == '.':
174 data.append(text[1:])
175 else:
176 data.append(text)
177 self.__data = NEWLINE.join(data)
178 status = self.__server.process_message(self.__peer,
179 self.__mailfrom,
180 self.__rcpttos,
181 self.__data)
182 self.__rcpttos = []
183 self.__mailfrom = None
184 self.__state = self.COMMAND
185 self.set_terminator('\r\n')
186 if not status:
187 self.push('250 Ok')
188 else:
189 self.push(status)
190
191 # SMTP and ESMTP commands
192 def smtp_HELO(self, arg):
193 if not arg:
194 self.push('501 Syntax: HELO hostname')
195 return
196 if self.__greeting:
197 self.push('503 Duplicate HELO/EHLO')
198 else:
199 self.__greeting = arg
200 self.push('250 %s' % self.__fqdn)
201
202 def smtp_NOOP(self, arg):
203 if arg:
204 self.push('501 Syntax: NOOP')
205 else:
206 self.push('250 Ok')
207
208 def smtp_QUIT(self, arg):
209 # args is ignored
210 self.push('221 Bye')
211 self.close_when_done()
212
213 # factored
214 def __getaddr(self, keyword, arg):
215 address = None
216 keylen = len(keyword)
217 if arg[:keylen].upper() == keyword:
218 address = arg[keylen:].strip()
219 if not address:
220 pass
221 elif address[0] == '<' and address[-1] == '>' and address != '<>':
222 # Addresses can be in the form <person@dom.com> but watch out
223 # for null address, e.g. <>
224 address = address[1:-1]
225 return address
226
227 def smtp_MAIL(self, arg):
228 print >> DEBUGSTREAM, '===> MAIL', arg
229 address = self.__getaddr('FROM:', arg) if arg else None
230 if not address:
231 self.push('501 Syntax: MAIL FROM:<address>')
232 return
233 if self.__mailfrom:
234 self.push('503 Error: nested MAIL command')
235 return
236 self.__mailfrom = address
237 print >> DEBUGSTREAM, 'sender:', self.__mailfrom
238 self.push('250 Ok')
239
240 def smtp_RCPT(self, arg):
241 print >> DEBUGSTREAM, '===> RCPT', arg
242 if not self.__mailfrom:
243 self.push('503 Error: need MAIL command')
244 return
245 address = self.__getaddr('TO:', arg) if arg else None
246 if not address:
247 self.push('501 Syntax: RCPT TO: <address>')
248 return
249 self.__rcpttos.append(address)
250 print >> DEBUGSTREAM, 'recips:', self.__rcpttos
251 self.push('250 Ok')
252
253 def smtp_RSET(self, arg):
254 if arg:
255 self.push('501 Syntax: RSET')
256 return
257 # Resets the sender, recipients, and data, but not the greeting
258 self.__mailfrom = None
259 self.__rcpttos = []
260 self.__data = ''
261 self.__state = self.COMMAND
262 self.push('250 Ok')
263
264 def smtp_DATA(self, arg):
265 if not self.__rcpttos:
266 self.push('503 Error: need RCPT command')
267 return
268 if arg:
269 self.push('501 Syntax: DATA')
270 return
271 self.__state = self.DATA
272 self.set_terminator('\r\n.\r\n')
273 self.push('354 End data with <CR><LF>.<CR><LF>')
274
275
276 class SMTPServer(asyncore.dispatcher):
277 def __init__(self, localaddr, remoteaddr):
278 self._localaddr = localaddr
279 self._remoteaddr = remoteaddr
280 asyncore.dispatcher.__init__(self)
281 try:
282 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
283 # try to re-use a server port if possible
284 self.set_reuse_addr()
285 self.bind(localaddr)
286 self.listen(5)
287 except:
288 # cleanup asyncore.socket_map before raising
289 self.close()
290 raise
291 else:
292 print >> DEBUGSTREAM, \
293 '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
294 self.__class__.__name__, time.ctime(time.time()),
295 localaddr, remoteaddr)
296
297 def handle_accept(self):
298 pair = self.accept()
299 if pair is not None:
300 conn, addr = pair
301 print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
302 channel = SMTPChannel(self, conn, addr)
303
304 # API for "doing something useful with the message"
305 def process_message(self, peer, mailfrom, rcpttos, data):
306 """Override this abstract method to handle messages from the client.
307
308 peer is a tuple containing (ipaddr, port) of the client that made the
309 socket connection to our smtp port.
310
311 mailfrom is the raw address the client claims the message is coming
312 from.
313
314 rcpttos is a list of raw addresses the client wishes to deliver the
315 message to.
316
317 data is a string containing the entire full text of the message,
318 headers (if supplied) and all. It has been `de-transparencied'
319 according to RFC 821, Section 4.5.2. In other words, a line
320 containing a `.' followed by other text has had the leading dot
321 removed.
322
323 This function should return None, for a normal `250 Ok' response;
324 otherwise it returns the desired response string in RFC 821 format.
325
326 """
327 raise NotImplementedError
328
329
330 class DebuggingServer(SMTPServer):
331 # Do something with the gathered message
332 def process_message(self, peer, mailfrom, rcpttos, data):
333 inheaders = 1
334 lines = data.split('\n')
335 print '---------- MESSAGE FOLLOWS ----------'
336 for line in lines:
337 # headers first
338 if inheaders and not line:
339 print 'X-Peer:', peer[0]
340 inheaders = 0
341 print line
342 print '------------ END MESSAGE ------------'
343
344
345 class PureProxy(SMTPServer):
346 def process_message(self, peer, mailfrom, rcpttos, data):
347 lines = data.split('\n')
348 # Look for the last header
349 i = 0
350 for line in lines:
351 if not line:
352 break
353 i += 1
354 lines.insert(i, 'X-Peer: %s' % peer[0])
355 data = NEWLINE.join(lines)
356 refused = self._deliver(mailfrom, rcpttos, data)
357 # TBD: what to do with refused addresses?
358 print >> DEBUGSTREAM, 'we got some refusals:', refused
359
360 def _deliver(self, mailfrom, rcpttos, data):
361 import smtplib
362 refused = {}
363 try:
364 s = smtplib.SMTP()
365 s.connect(self._remoteaddr[0], self._remoteaddr[1])
366 try:
367 refused = s.sendmail(mailfrom, rcpttos, data)
368 finally:
369 s.quit()
370 except smtplib.SMTPRecipientsRefused, e:
371 print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
372 refused = e.recipients
373 except (socket.error, smtplib.SMTPException), e:
374 print >> DEBUGSTREAM, 'got', e.__class__
375 # All recipients were refused. If the exception had an associated
376 # error code, use it. Otherwise,fake it with a non-triggering
377 # exception code.
378 errcode = getattr(e, 'smtp_code', -1)
379 errmsg = getattr(e, 'smtp_error', 'ignore')
380 for r in rcpttos:
381 refused[r] = (errcode, errmsg)
382 return refused
383
384
385 class MailmanProxy(PureProxy):
386 def process_message(self, peer, mailfrom, rcpttos, data):
387 from cStringIO import StringIO
388 from Mailman import Utils
389 from Mailman import Message
390 from Mailman import MailList
391 # If the message is to a Mailman mailing list, then we'll invoke the
392 # Mailman script directly, without going through the real smtpd.
393 # Otherwise we'll forward it to the local proxy for disposition.
394 listnames = []
395 for rcpt in rcpttos:
396 local = rcpt.lower().split('@')[0]
397 # We allow the following variations on the theme
398 # listname
399 # listname-admin
400 # listname-owner
401 # listname-request
402 # listname-join
403 # listname-leave
404 parts = local.split('-')
405 if len(parts) > 2:
406 continue
407 listname = parts[0]
408 if len(parts) == 2:
409 command = parts[1]
410 else:
411 command = ''
412 if not Utils.list_exists(listname) or command not in (
413 '', 'admin', 'owner', 'request', 'join', 'leave'):
414 continue
415 listnames.append((rcpt, listname, command))
416 # Remove all list recipients from rcpttos and forward what we're not
417 # going to take care of ourselves. Linear removal should be fine
418 # since we don't expect a large number of recipients.
419 for rcpt, listname, command in listnames:
420 rcpttos.remove(rcpt)
421 # If there's any non-list destined recipients left,
422 print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
423 if rcpttos:
424 refused = self._deliver(mailfrom, rcpttos, data)
425 # TBD: what to do with refused addresses?
426 print >> DEBUGSTREAM, 'we got refusals:', refused
427 # Now deliver directly to the list commands
428 mlists = {}
429 s = StringIO(data)
430 msg = Message.Message(s)
431 # These headers are required for the proper execution of Mailman. All
432 # MTAs in existence seem to add these if the original message doesn't
433 # have them.
434 if not msg.getheader('from'):
435 msg['From'] = mailfrom
436 if not msg.getheader('date'):
437 msg['Date'] = time.ctime(time.time())
438 for rcpt, listname, command in listnames:
439 print >> DEBUGSTREAM, 'sending message to', rcpt
440 mlist = mlists.get(listname)
441 if not mlist:
442 mlist = MailList.MailList(listname, lock=0)
443 mlists[listname] = mlist
444 # dispatch on the type of command
445 if command == '':
446 # post
447 msg.Enqueue(mlist, tolist=1)
448 elif command == 'admin':
449 msg.Enqueue(mlist, toadmin=1)
450 elif command == 'owner':
451 msg.Enqueue(mlist, toowner=1)
452 elif command == 'request':
453 msg.Enqueue(mlist, torequest=1)
454 elif command in ('join', 'leave'):
455 # TBD: this is a hack!
456 if command == 'join':
457 msg['Subject'] = 'subscribe'
458 else:
459 msg['Subject'] = 'unsubscribe'
460 msg.Enqueue(mlist, torequest=1)
461
462
463 class Options:
464 setuid = 1
465 classname = 'PureProxy'
466
467
468 def parseargs():
469 global DEBUGSTREAM
470 try:
471 opts, args = getopt.getopt(
472 sys.argv[1:], 'nVhc:d',
473 ['class=', 'nosetuid', 'version', 'help', 'debug'])
474 except getopt.error, e:
475 usage(1, e)
476
477 options = Options()
478 for opt, arg in opts:
479 if opt in ('-h', '--help'):
480 usage(0)
481 elif opt in ('-V', '--version'):
482 print >> sys.stderr, __version__
483 sys.exit(0)
484 elif opt in ('-n', '--nosetuid'):
485 options.setuid = 0
486 elif opt in ('-c', '--class'):
487 options.classname = arg
488 elif opt in ('-d', '--debug'):
489 DEBUGSTREAM = sys.stderr
490
491 # parse the rest of the arguments
492 if len(args) < 1:
493 localspec = 'localhost:8025'
494 remotespec = 'localhost:25'
495 elif len(args) < 2:
496 localspec = args[0]
497 remotespec = 'localhost:25'
498 elif len(args) < 3:
499 localspec = args[0]
500 remotespec = args[1]
501 else:
502 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
503
504 # split into host/port pairs
505 i = localspec.find(':')
506 if i < 0:
507 usage(1, 'Bad local spec: %s' % localspec)
508 options.localhost = localspec[:i]
509 try:
510 options.localport = int(localspec[i+1:])
511 except ValueError:
512 usage(1, 'Bad local port: %s' % localspec)
513 i = remotespec.find(':')
514 if i < 0:
515 usage(1, 'Bad remote spec: %s' % remotespec)
516 options.remotehost = remotespec[:i]
517 try:
518 options.remoteport = int(remotespec[i+1:])
519 except ValueError:
520 usage(1, 'Bad remote port: %s' % remotespec)
521 return options
522
523
524 if __name__ == '__main__':
525 options = parseargs()
526 # Become nobody
527 if options.setuid:
528 try:
529 import pwd
530 except ImportError:
531 print >> sys.stderr, \
532 'Cannot import module "pwd"; try running with -n option.'
533 sys.exit(1)
534 nobody = pwd.getpwnam('nobody')[2]
535 try:
536 os.setuid(nobody)
537 except OSError, e:
538 if e.errno != errno.EPERM: raise
539 print >> sys.stderr, \
540 'Cannot setuid "nobody"; try running with -n option.'
541 sys.exit(1)
542 classname = options.classname
543 if "." in classname:
544 lastdot = classname.rfind(".")
545 mod = __import__(classname[:lastdot], globals(), locals(), [""])
546 classname = classname[lastdot+1:]
547 else:
548 import __main__ as mod
549 class_ = getattr(mod, classname)
550 proxy = class_((options.localhost, options.localport),
551 (options.remotehost, options.remoteport))
552 try:
553 asyncore.loop()
554 except KeyboardInterrupt:
555 pass