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