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