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