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()