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