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