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