##// END OF EJS Templates
smtp: add the class to verify the certificate of the SMTP server for STARTTLS...
FUJIWARA Katsunori -
r18885:cf1304fb default
parent child Browse files
Show More
@@ -1,255 +1,282
1 # mail.py - mail sending bits for mercurial
1 # mail.py - mail sending bits for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 import util, encoding
9 import util, encoding, sslutil
10 import os, smtplib, socket, quopri, time
10 import os, smtplib, socket, quopri, time
11 import email.Header, email.MIMEText, email.Utils
11 import email.Header, email.MIMEText, email.Utils
12
12
13 _oldheaderinit = email.Header.Header.__init__
13 _oldheaderinit = email.Header.Header.__init__
14 def _unifiedheaderinit(self, *args, **kw):
14 def _unifiedheaderinit(self, *args, **kw):
15 """
15 """
16 Python 2.7 introduces a backwards incompatible change
16 Python 2.7 introduces a backwards incompatible change
17 (Python issue1974, r70772) in email.Generator.Generator code:
17 (Python issue1974, r70772) in email.Generator.Generator code:
18 pre-2.7 code passed "continuation_ws='\t'" to the Header
18 pre-2.7 code passed "continuation_ws='\t'" to the Header
19 constructor, and 2.7 removed this parameter.
19 constructor, and 2.7 removed this parameter.
20
20
21 Default argument is continuation_ws=' ', which means that the
21 Default argument is continuation_ws=' ', which means that the
22 behaviour is different in <2.7 and 2.7
22 behaviour is different in <2.7 and 2.7
23
23
24 We consider the 2.7 behaviour to be preferable, but need
24 We consider the 2.7 behaviour to be preferable, but need
25 to have an unified behaviour for versions 2.4 to 2.7
25 to have an unified behaviour for versions 2.4 to 2.7
26 """
26 """
27 # override continuation_ws
27 # override continuation_ws
28 kw['continuation_ws'] = ' '
28 kw['continuation_ws'] = ' '
29 _oldheaderinit(self, *args, **kw)
29 _oldheaderinit(self, *args, **kw)
30
30
31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
32
32
33 class STARTTLS(smtplib.SMTP):
34 '''Derived class to verify the peer certificate for STARTTLS.
35
36 This class allows to pass any keyword arguments to SSL socket creation.
37 '''
38 def __init__(self, sslkwargs, **kwargs):
39 smtplib.SMTP.__init__(self, **kwargs)
40 self._sslkwargs = sslkwargs
41
42 def starttls(self, keyfile=None, certfile=None):
43 if not self.has_extn("starttls"):
44 msg = "STARTTLS extension not supported by server"
45 raise smtplib.SMTPException(msg)
46 (resp, reply) = self.docmd("STARTTLS")
47 if resp == 220:
48 self.sock = sslutil.ssl_wrap_socket(self.sock, keyfile, certfile,
49 **self._sslkwargs)
50 if not util.safehasattr(self.sock, "read"):
51 # using httplib.FakeSocket with Python 2.5.x or earlier
52 self.sock.read = self.sock.recv
53 self.file = smtplib.SSLFakeFile(self.sock)
54 self.helo_resp = None
55 self.ehlo_resp = None
56 self.esmtp_features = {}
57 self.does_esmtp = 0
58 return (resp, reply)
59
33 def _smtp(ui):
60 def _smtp(ui):
34 '''build an smtp connection and return a function to send mail'''
61 '''build an smtp connection and return a function to send mail'''
35 local_hostname = ui.config('smtp', 'local_hostname')
62 local_hostname = ui.config('smtp', 'local_hostname')
36 tls = ui.config('smtp', 'tls', 'none')
63 tls = ui.config('smtp', 'tls', 'none')
37 # backward compatible: when tls = true, we use starttls.
64 # backward compatible: when tls = true, we use starttls.
38 starttls = tls == 'starttls' or util.parsebool(tls)
65 starttls = tls == 'starttls' or util.parsebool(tls)
39 smtps = tls == 'smtps'
66 smtps = tls == 'smtps'
40 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
67 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
41 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
68 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
42 if smtps:
69 if smtps:
43 ui.note(_('(using smtps)\n'))
70 ui.note(_('(using smtps)\n'))
44 s = smtplib.SMTP_SSL(local_hostname=local_hostname)
71 s = smtplib.SMTP_SSL(local_hostname=local_hostname)
45 else:
72 else:
46 s = smtplib.SMTP(local_hostname=local_hostname)
73 s = smtplib.SMTP(local_hostname=local_hostname)
47 mailhost = ui.config('smtp', 'host')
74 mailhost = ui.config('smtp', 'host')
48 if not mailhost:
75 if not mailhost:
49 raise util.Abort(_('smtp.host not configured - cannot send mail'))
76 raise util.Abort(_('smtp.host not configured - cannot send mail'))
50 mailport = util.getport(ui.config('smtp', 'port', 25))
77 mailport = util.getport(ui.config('smtp', 'port', 25))
51 ui.note(_('sending mail: smtp host %s, port %s\n') %
78 ui.note(_('sending mail: smtp host %s, port %s\n') %
52 (mailhost, mailport))
79 (mailhost, mailport))
53 s.connect(host=mailhost, port=mailport)
80 s.connect(host=mailhost, port=mailport)
54 if starttls:
81 if starttls:
55 ui.note(_('(using starttls)\n'))
82 ui.note(_('(using starttls)\n'))
56 s.ehlo()
83 s.ehlo()
57 s.starttls()
84 s.starttls()
58 s.ehlo()
85 s.ehlo()
59 username = ui.config('smtp', 'username')
86 username = ui.config('smtp', 'username')
60 password = ui.config('smtp', 'password')
87 password = ui.config('smtp', 'password')
61 if username and not password:
88 if username and not password:
62 password = ui.getpass()
89 password = ui.getpass()
63 if username and password:
90 if username and password:
64 ui.note(_('(authenticating to mail server as %s)\n') %
91 ui.note(_('(authenticating to mail server as %s)\n') %
65 (username))
92 (username))
66 try:
93 try:
67 s.login(username, password)
94 s.login(username, password)
68 except smtplib.SMTPException, inst:
95 except smtplib.SMTPException, inst:
69 raise util.Abort(inst)
96 raise util.Abort(inst)
70
97
71 def send(sender, recipients, msg):
98 def send(sender, recipients, msg):
72 try:
99 try:
73 return s.sendmail(sender, recipients, msg)
100 return s.sendmail(sender, recipients, msg)
74 except smtplib.SMTPRecipientsRefused, inst:
101 except smtplib.SMTPRecipientsRefused, inst:
75 recipients = [r[1] for r in inst.recipients.values()]
102 recipients = [r[1] for r in inst.recipients.values()]
76 raise util.Abort('\n' + '\n'.join(recipients))
103 raise util.Abort('\n' + '\n'.join(recipients))
77 except smtplib.SMTPException, inst:
104 except smtplib.SMTPException, inst:
78 raise util.Abort(inst)
105 raise util.Abort(inst)
79
106
80 return send
107 return send
81
108
82 def _sendmail(ui, sender, recipients, msg):
109 def _sendmail(ui, sender, recipients, msg):
83 '''send mail using sendmail.'''
110 '''send mail using sendmail.'''
84 program = ui.config('email', 'method')
111 program = ui.config('email', 'method')
85 cmdline = '%s -f %s %s' % (program, util.email(sender),
112 cmdline = '%s -f %s %s' % (program, util.email(sender),
86 ' '.join(map(util.email, recipients)))
113 ' '.join(map(util.email, recipients)))
87 ui.note(_('sending mail: %s\n') % cmdline)
114 ui.note(_('sending mail: %s\n') % cmdline)
88 fp = util.popen(cmdline, 'w')
115 fp = util.popen(cmdline, 'w')
89 fp.write(msg)
116 fp.write(msg)
90 ret = fp.close()
117 ret = fp.close()
91 if ret:
118 if ret:
92 raise util.Abort('%s %s' % (
119 raise util.Abort('%s %s' % (
93 os.path.basename(program.split(None, 1)[0]),
120 os.path.basename(program.split(None, 1)[0]),
94 util.explainexit(ret)[0]))
121 util.explainexit(ret)[0]))
95
122
96 def _mbox(mbox, sender, recipients, msg):
123 def _mbox(mbox, sender, recipients, msg):
97 '''write mails to mbox'''
124 '''write mails to mbox'''
98 fp = open(mbox, 'ab+')
125 fp = open(mbox, 'ab+')
99 # Should be time.asctime(), but Windows prints 2-characters day
126 # Should be time.asctime(), but Windows prints 2-characters day
100 # of month instead of one. Make them print the same thing.
127 # of month instead of one. Make them print the same thing.
101 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
128 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
102 fp.write('From %s %s\n' % (sender, date))
129 fp.write('From %s %s\n' % (sender, date))
103 fp.write(msg)
130 fp.write(msg)
104 fp.write('\n\n')
131 fp.write('\n\n')
105 fp.close()
132 fp.close()
106
133
107 def connect(ui, mbox=None):
134 def connect(ui, mbox=None):
108 '''make a mail connection. return a function to send mail.
135 '''make a mail connection. return a function to send mail.
109 call as sendmail(sender, list-of-recipients, msg).'''
136 call as sendmail(sender, list-of-recipients, msg).'''
110 if mbox:
137 if mbox:
111 open(mbox, 'wb').close()
138 open(mbox, 'wb').close()
112 return lambda s, r, m: _mbox(mbox, s, r, m)
139 return lambda s, r, m: _mbox(mbox, s, r, m)
113 if ui.config('email', 'method', 'smtp') == 'smtp':
140 if ui.config('email', 'method', 'smtp') == 'smtp':
114 return _smtp(ui)
141 return _smtp(ui)
115 return lambda s, r, m: _sendmail(ui, s, r, m)
142 return lambda s, r, m: _sendmail(ui, s, r, m)
116
143
117 def sendmail(ui, sender, recipients, msg, mbox=None):
144 def sendmail(ui, sender, recipients, msg, mbox=None):
118 send = connect(ui, mbox=mbox)
145 send = connect(ui, mbox=mbox)
119 return send(sender, recipients, msg)
146 return send(sender, recipients, msg)
120
147
121 def validateconfig(ui):
148 def validateconfig(ui):
122 '''determine if we have enough config data to try sending email.'''
149 '''determine if we have enough config data to try sending email.'''
123 method = ui.config('email', 'method', 'smtp')
150 method = ui.config('email', 'method', 'smtp')
124 if method == 'smtp':
151 if method == 'smtp':
125 if not ui.config('smtp', 'host'):
152 if not ui.config('smtp', 'host'):
126 raise util.Abort(_('smtp specified as email transport, '
153 raise util.Abort(_('smtp specified as email transport, '
127 'but no smtp host configured'))
154 'but no smtp host configured'))
128 else:
155 else:
129 if not util.findexe(method):
156 if not util.findexe(method):
130 raise util.Abort(_('%r specified as email transport, '
157 raise util.Abort(_('%r specified as email transport, '
131 'but not in PATH') % method)
158 'but not in PATH') % method)
132
159
133 def mimetextpatch(s, subtype='plain', display=False):
160 def mimetextpatch(s, subtype='plain', display=False):
134 '''Return MIME message suitable for a patch.
161 '''Return MIME message suitable for a patch.
135 Charset will be detected as utf-8 or (possibly fake) us-ascii.
162 Charset will be detected as utf-8 or (possibly fake) us-ascii.
136 Transfer encodings will be used if necessary.'''
163 Transfer encodings will be used if necessary.'''
137
164
138 cs = 'us-ascii'
165 cs = 'us-ascii'
139 if not display:
166 if not display:
140 try:
167 try:
141 s.decode('us-ascii')
168 s.decode('us-ascii')
142 except UnicodeDecodeError:
169 except UnicodeDecodeError:
143 try:
170 try:
144 s.decode('utf-8')
171 s.decode('utf-8')
145 cs = 'utf-8'
172 cs = 'utf-8'
146 except UnicodeDecodeError:
173 except UnicodeDecodeError:
147 # We'll go with us-ascii as a fallback.
174 # We'll go with us-ascii as a fallback.
148 pass
175 pass
149
176
150 return mimetextqp(s, subtype, cs)
177 return mimetextqp(s, subtype, cs)
151
178
152 def mimetextqp(body, subtype, charset):
179 def mimetextqp(body, subtype, charset):
153 '''Return MIME message.
180 '''Return MIME message.
154 Quoted-printable transfer encoding will be used if necessary.
181 Quoted-printable transfer encoding will be used if necessary.
155 '''
182 '''
156 enc = None
183 enc = None
157 for line in body.splitlines():
184 for line in body.splitlines():
158 if len(line) > 950:
185 if len(line) > 950:
159 body = quopri.encodestring(body)
186 body = quopri.encodestring(body)
160 enc = "quoted-printable"
187 enc = "quoted-printable"
161 break
188 break
162
189
163 msg = email.MIMEText.MIMEText(body, subtype, charset)
190 msg = email.MIMEText.MIMEText(body, subtype, charset)
164 if enc:
191 if enc:
165 del msg['Content-Transfer-Encoding']
192 del msg['Content-Transfer-Encoding']
166 msg['Content-Transfer-Encoding'] = enc
193 msg['Content-Transfer-Encoding'] = enc
167 return msg
194 return msg
168
195
169 def _charsets(ui):
196 def _charsets(ui):
170 '''Obtains charsets to send mail parts not containing patches.'''
197 '''Obtains charsets to send mail parts not containing patches.'''
171 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
198 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
172 fallbacks = [encoding.fallbackencoding.lower(),
199 fallbacks = [encoding.fallbackencoding.lower(),
173 encoding.encoding.lower(), 'utf-8']
200 encoding.encoding.lower(), 'utf-8']
174 for cs in fallbacks: # find unique charsets while keeping order
201 for cs in fallbacks: # find unique charsets while keeping order
175 if cs not in charsets:
202 if cs not in charsets:
176 charsets.append(cs)
203 charsets.append(cs)
177 return [cs for cs in charsets if not cs.endswith('ascii')]
204 return [cs for cs in charsets if not cs.endswith('ascii')]
178
205
179 def _encode(ui, s, charsets):
206 def _encode(ui, s, charsets):
180 '''Returns (converted) string, charset tuple.
207 '''Returns (converted) string, charset tuple.
181 Finds out best charset by cycling through sendcharsets in descending
208 Finds out best charset by cycling through sendcharsets in descending
182 order. Tries both encoding and fallbackencoding for input. Only as
209 order. Tries both encoding and fallbackencoding for input. Only as
183 last resort send as is in fake ascii.
210 last resort send as is in fake ascii.
184 Caveat: Do not use for mail parts containing patches!'''
211 Caveat: Do not use for mail parts containing patches!'''
185 try:
212 try:
186 s.decode('ascii')
213 s.decode('ascii')
187 except UnicodeDecodeError:
214 except UnicodeDecodeError:
188 sendcharsets = charsets or _charsets(ui)
215 sendcharsets = charsets or _charsets(ui)
189 for ics in (encoding.encoding, encoding.fallbackencoding):
216 for ics in (encoding.encoding, encoding.fallbackencoding):
190 try:
217 try:
191 u = s.decode(ics)
218 u = s.decode(ics)
192 except UnicodeDecodeError:
219 except UnicodeDecodeError:
193 continue
220 continue
194 for ocs in sendcharsets:
221 for ocs in sendcharsets:
195 try:
222 try:
196 return u.encode(ocs), ocs
223 return u.encode(ocs), ocs
197 except UnicodeEncodeError:
224 except UnicodeEncodeError:
198 pass
225 pass
199 except LookupError:
226 except LookupError:
200 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
227 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
201 # if ascii, or all conversion attempts fail, send (broken) ascii
228 # if ascii, or all conversion attempts fail, send (broken) ascii
202 return s, 'us-ascii'
229 return s, 'us-ascii'
203
230
204 def headencode(ui, s, charsets=None, display=False):
231 def headencode(ui, s, charsets=None, display=False):
205 '''Returns RFC-2047 compliant header from given string.'''
232 '''Returns RFC-2047 compliant header from given string.'''
206 if not display:
233 if not display:
207 # split into words?
234 # split into words?
208 s, cs = _encode(ui, s, charsets)
235 s, cs = _encode(ui, s, charsets)
209 return str(email.Header.Header(s, cs))
236 return str(email.Header.Header(s, cs))
210 return s
237 return s
211
238
212 def _addressencode(ui, name, addr, charsets=None):
239 def _addressencode(ui, name, addr, charsets=None):
213 name = headencode(ui, name, charsets)
240 name = headencode(ui, name, charsets)
214 try:
241 try:
215 acc, dom = addr.split('@')
242 acc, dom = addr.split('@')
216 acc = acc.encode('ascii')
243 acc = acc.encode('ascii')
217 dom = dom.decode(encoding.encoding).encode('idna')
244 dom = dom.decode(encoding.encoding).encode('idna')
218 addr = '%s@%s' % (acc, dom)
245 addr = '%s@%s' % (acc, dom)
219 except UnicodeDecodeError:
246 except UnicodeDecodeError:
220 raise util.Abort(_('invalid email address: %s') % addr)
247 raise util.Abort(_('invalid email address: %s') % addr)
221 except ValueError:
248 except ValueError:
222 try:
249 try:
223 # too strict?
250 # too strict?
224 addr = addr.encode('ascii')
251 addr = addr.encode('ascii')
225 except UnicodeDecodeError:
252 except UnicodeDecodeError:
226 raise util.Abort(_('invalid local address: %s') % addr)
253 raise util.Abort(_('invalid local address: %s') % addr)
227 return email.Utils.formataddr((name, addr))
254 return email.Utils.formataddr((name, addr))
228
255
229 def addressencode(ui, address, charsets=None, display=False):
256 def addressencode(ui, address, charsets=None, display=False):
230 '''Turns address into RFC-2047 compliant header.'''
257 '''Turns address into RFC-2047 compliant header.'''
231 if display or not address:
258 if display or not address:
232 return address or ''
259 return address or ''
233 name, addr = email.Utils.parseaddr(address)
260 name, addr = email.Utils.parseaddr(address)
234 return _addressencode(ui, name, addr, charsets)
261 return _addressencode(ui, name, addr, charsets)
235
262
236 def addrlistencode(ui, addrs, charsets=None, display=False):
263 def addrlistencode(ui, addrs, charsets=None, display=False):
237 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
264 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
238 A single element of input list may contain multiple addresses, but output
265 A single element of input list may contain multiple addresses, but output
239 always has one address per item'''
266 always has one address per item'''
240 if display:
267 if display:
241 return [a.strip() for a in addrs if a.strip()]
268 return [a.strip() for a in addrs if a.strip()]
242
269
243 result = []
270 result = []
244 for name, addr in email.Utils.getaddresses(addrs):
271 for name, addr in email.Utils.getaddresses(addrs):
245 if name or addr:
272 if name or addr:
246 result.append(_addressencode(ui, name, addr, charsets))
273 result.append(_addressencode(ui, name, addr, charsets))
247 return result
274 return result
248
275
249 def mimeencode(ui, s, charsets=None, display=False):
276 def mimeencode(ui, s, charsets=None, display=False):
250 '''creates mime text object, encodes it if needed, and sets
277 '''creates mime text object, encodes it if needed, and sets
251 charset and transfer-encoding accordingly.'''
278 charset and transfer-encoding accordingly.'''
252 cs = 'us-ascii'
279 cs = 'us-ascii'
253 if not display:
280 if not display:
254 s, cs = _encode(ui, s, charsets)
281 s, cs = _encode(ui, s, charsets)
255 return mimetextqp(s, 'plain', cs)
282 return mimetextqp(s, 'plain', cs)
General Comments 0
You need to be logged in to leave comments. Login now