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