Mercurial > hg
comparison tests/dummysmtpd.py @ 50751:0a55206c5a1e
branching: merge stable into default
author | Raphaël Gomès <rgomes@octobus.net> |
---|---|
date | Thu, 06 Jul 2023 16:07:34 +0200 |
parents | b3a5af04da35 |
children | 8f0b0df79039 |
comparison
equal
deleted
inserted
replaced
50718:0ab3956540a6 | 50751:0a55206c5a1e |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 """dummy SMTP server for use in tests""" | 3 """dummy SMTP server for use in tests""" |
4 | 4 |
5 | 5 |
6 import asyncore | |
7 import optparse | 6 import optparse |
8 import smtpd | 7 import os |
8 import socket | |
9 import ssl | 9 import ssl |
10 import sys | 10 import sys |
11 import traceback | |
12 | 11 |
13 from mercurial import ( | 12 from mercurial import ( |
14 pycompat, | 13 pycompat, |
15 server, | 14 server, |
16 sslutil, | 15 sslutil, |
17 ui as uimod, | 16 ui as uimod, |
18 ) | 17 ) |
19 | 18 |
20 | 19 |
20 if os.environ.get('HGIPV6', '0') == '1': | |
21 family = socket.AF_INET6 | |
22 else: | |
23 family = socket.AF_INET | |
24 | |
25 | |
21 def log(msg): | 26 def log(msg): |
22 sys.stdout.write(msg) | 27 sys.stdout.write(msg) |
23 sys.stdout.flush() | 28 sys.stdout.flush() |
24 | 29 |
25 | 30 |
26 class dummysmtpserver(smtpd.SMTPServer): | 31 def mocksmtpserversession(conn, addr): |
27 def __init__(self, localaddr): | 32 conn.send(b'220 smtp.example.com ESMTP\r\n') |
28 smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None) | |
29 | 33 |
30 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): | 34 line = conn.recv(1024) |
31 log('%s from=%s to=%s\n' % (peer[0], mailfrom, ', '.join(rcpttos))) | 35 if not line.lower().startswith(b'ehlo '): |
36 log('no hello: %s\n' % line) | |
37 return | |
32 | 38 |
33 def handle_error(self): | 39 conn.send(b'250 Hello\r\n') |
34 # On Windows, a bad SSL connection sometimes generates a WSAECONNRESET. | 40 |
35 # The default handler will shutdown this server, and then both the | 41 line = conn.recv(1024) |
36 # current connection and subsequent ones fail on the client side with | 42 if not line.lower().startswith(b'mail from:'): |
37 # "No connection could be made because the target machine actively | 43 log('no mail from: %s\n' % line) |
38 # refused it". If we eat the error, then the client properly aborts in | 44 return |
39 # the expected way, and the server is available for subsequent requests. | 45 mailfrom = line[10:].decode().rstrip() |
40 traceback.print_exc() | 46 if mailfrom.startswith('<') and mailfrom.endswith('>'): |
47 mailfrom = mailfrom[1:-1] | |
48 | |
49 conn.send(b'250 Ok\r\n') | |
50 | |
51 rcpttos = [] | |
52 while True: | |
53 line = conn.recv(1024) | |
54 if not line.lower().startswith(b'rcpt to:'): | |
55 break | |
56 rcptto = line[8:].decode().rstrip() | |
57 if rcptto.startswith('<') and rcptto.endswith('>'): | |
58 rcptto = rcptto[1:-1] | |
59 rcpttos.append(rcptto) | |
60 | |
61 conn.send(b'250 Ok\r\n') | |
62 | |
63 if not line.lower().strip() == b'data': | |
64 log('no rcpt to or data: %s' % line) | |
65 | |
66 conn.send(b'354 Go ahead\r\n') | |
67 | |
68 data = b'' | |
69 while True: | |
70 line = conn.recv(1024) | |
71 if not line: | |
72 log('connection closed before end of data') | |
73 break | |
74 data += line | |
75 if data.endswith(b'\r\n.\r\n'): | |
76 data = data[:-5] | |
77 break | |
78 | |
79 conn.send(b'250 Ok\r\n') | |
80 | |
81 log( | |
82 '%s from=%s to=%s\n%s\n' | |
83 % (addr[0], mailfrom, ', '.join(rcpttos), data.decode()) | |
84 ) | |
41 | 85 |
42 | 86 |
43 class dummysmtpsecureserver(dummysmtpserver): | 87 def run(host, port, certificate): |
44 def __init__(self, localaddr, certfile): | 88 ui = uimod.ui.load() |
45 dummysmtpserver.__init__(self, localaddr) | 89 with socket.socket(family, socket.SOCK_STREAM) as s: |
46 self._certfile = certfile | 90 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
47 | 91 s.bind((host, port)) |
48 def handle_accept(self): | 92 # log('listening at %s:%d\n' % (host, port)) |
49 pair = self.accept() | 93 s.listen(1) |
50 if not pair: | |
51 return | |
52 conn, addr = pair | |
53 ui = uimod.ui.load() | |
54 try: | 94 try: |
55 # wrap_socket() would block, but we don't care | 95 while True: |
56 conn = sslutil.wrapserversocket(conn, ui, certfile=self._certfile) | 96 conn, addr = s.accept() |
57 except ssl.SSLError: | 97 if certificate: |
58 log('%s ssl error\n' % addr[0]) | 98 try: |
59 conn.close() | 99 conn = sslutil.wrapserversocket( |
60 return | 100 conn, ui, certfile=certificate |
61 smtpd.SMTPChannel(self, conn, addr) | 101 ) |
62 | 102 except ssl.SSLError as e: |
63 | 103 log('%s ssl error: %s\n' % (addr[0], e)) |
64 def run(): | 104 conn.close() |
65 try: | 105 continue |
66 asyncore.loop() | 106 log("connection from %s:%s\n" % addr) |
67 except KeyboardInterrupt: | 107 mocksmtpserversession(conn, addr) |
68 pass | 108 conn.close() |
109 except KeyboardInterrupt: | |
110 pass | |
69 | 111 |
70 | 112 |
71 def _encodestrsonly(v): | 113 def _encodestrsonly(v): |
72 if isinstance(v, type(u'')): | 114 if isinstance(v, type(u'')): |
73 return v.encode('ascii') | 115 return v.encode('ascii') |
91 op.add_option('-p', '--port', type=int, default=8025) | 133 op.add_option('-p', '--port', type=int, default=8025) |
92 op.add_option('-a', '--address', default='localhost') | 134 op.add_option('-a', '--address', default='localhost') |
93 op.add_option('--pid-file', metavar='FILE') | 135 op.add_option('--pid-file', metavar='FILE') |
94 op.add_option('--tls', choices=['none', 'smtps'], default='none') | 136 op.add_option('--tls', choices=['none', 'smtps'], default='none') |
95 op.add_option('--certificate', metavar='FILE') | 137 op.add_option('--certificate', metavar='FILE') |
138 op.add_option('--logfile', metavar='FILE') | |
96 | 139 |
97 opts, args = op.parse_args() | 140 opts, args = op.parse_args() |
98 if opts.tls == 'smtps' and not opts.certificate: | 141 if (opts.tls == 'smtps') != bool(opts.certificate): |
99 op.error('--certificate must be specified') | 142 op.error('--certificate must be specified with --tls=smtps') |
100 | |
101 addr = (opts.address, opts.port) | |
102 | |
103 def init(): | |
104 if opts.tls == 'none': | |
105 dummysmtpserver(addr) | |
106 else: | |
107 dummysmtpsecureserver(addr, opts.certificate) | |
108 log('listening at %s:%d\n' % addr) | |
109 | 143 |
110 server.runservice( | 144 server.runservice( |
111 bytesvars(opts), | 145 bytesvars(opts), |
112 initfn=init, | 146 runfn=lambda: run(opts.address, opts.port, opts.certificate), |
113 runfn=run, | |
114 runargs=[pycompat.sysexecutable, pycompat.fsencode(__file__)] | 147 runargs=[pycompat.sysexecutable, pycompat.fsencode(__file__)] |
115 + pycompat.sysargv[1:], | 148 + pycompat.sysargv[1:], |
149 logfile=opts.logfile, | |
116 ) | 150 ) |
117 | 151 |
118 | 152 |
119 if __name__ == '__main__': | 153 if __name__ == '__main__': |
120 main() | 154 main() |