PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/External/Python/Lib/smtpd.py

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