104 return False |
104 return False |
105 |
105 |
106 |
106 |
107 def _smtp(ui): |
107 def _smtp(ui): |
108 '''build an smtp connection and return a function to send mail''' |
108 '''build an smtp connection and return a function to send mail''' |
109 local_hostname = ui.config('smtp', 'local_hostname') |
109 local_hostname = ui.config(b'smtp', b'local_hostname') |
110 tls = ui.config('smtp', 'tls') |
110 tls = ui.config(b'smtp', b'tls') |
111 # backward compatible: when tls = true, we use starttls. |
111 # backward compatible: when tls = true, we use starttls. |
112 starttls = tls == 'starttls' or stringutil.parsebool(tls) |
112 starttls = tls == b'starttls' or stringutil.parsebool(tls) |
113 smtps = tls == 'smtps' |
113 smtps = tls == b'smtps' |
114 if (starttls or smtps) and not _pyhastls(): |
114 if (starttls or smtps) and not _pyhastls(): |
115 raise error.Abort(_("can't use TLS: Python SSL support not installed")) |
115 raise error.Abort(_(b"can't use TLS: Python SSL support not installed")) |
116 mailhost = ui.config('smtp', 'host') |
116 mailhost = ui.config(b'smtp', b'host') |
117 if not mailhost: |
117 if not mailhost: |
118 raise error.Abort(_('smtp.host not configured - cannot send mail')) |
118 raise error.Abort(_(b'smtp.host not configured - cannot send mail')) |
119 if smtps: |
119 if smtps: |
120 ui.note(_('(using smtps)\n')) |
120 ui.note(_(b'(using smtps)\n')) |
121 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost) |
121 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost) |
122 elif starttls: |
122 elif starttls: |
123 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost) |
123 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost) |
124 else: |
124 else: |
125 s = smtplib.SMTP(local_hostname=local_hostname) |
125 s = smtplib.SMTP(local_hostname=local_hostname) |
126 if smtps: |
126 if smtps: |
127 defaultport = 465 |
127 defaultport = 465 |
128 else: |
128 else: |
129 defaultport = 25 |
129 defaultport = 25 |
130 mailport = util.getport(ui.config('smtp', 'port', defaultport)) |
130 mailport = util.getport(ui.config(b'smtp', b'port', defaultport)) |
131 ui.note(_('sending mail: smtp host %s, port %d\n') % (mailhost, mailport)) |
131 ui.note(_(b'sending mail: smtp host %s, port %d\n') % (mailhost, mailport)) |
132 s.connect(host=mailhost, port=mailport) |
132 s.connect(host=mailhost, port=mailport) |
133 if starttls: |
133 if starttls: |
134 ui.note(_('(using starttls)\n')) |
134 ui.note(_(b'(using starttls)\n')) |
135 s.ehlo() |
135 s.ehlo() |
136 s.starttls() |
136 s.starttls() |
137 s.ehlo() |
137 s.ehlo() |
138 if starttls or smtps: |
138 if starttls or smtps: |
139 ui.note(_('(verifying remote certificate)\n')) |
139 ui.note(_(b'(verifying remote certificate)\n')) |
140 sslutil.validatesocket(s.sock) |
140 sslutil.validatesocket(s.sock) |
141 username = ui.config('smtp', 'username') |
141 username = ui.config(b'smtp', b'username') |
142 password = ui.config('smtp', 'password') |
142 password = ui.config(b'smtp', b'password') |
143 if username and not password: |
143 if username and not password: |
144 password = ui.getpass() |
144 password = ui.getpass() |
145 if username and password: |
145 if username and password: |
146 ui.note(_('(authenticating to mail server as %s)\n') % username) |
146 ui.note(_(b'(authenticating to mail server as %s)\n') % username) |
147 try: |
147 try: |
148 s.login(username, password) |
148 s.login(username, password) |
149 except smtplib.SMTPException as inst: |
149 except smtplib.SMTPException as inst: |
150 raise error.Abort(inst) |
150 raise error.Abort(inst) |
151 |
151 |
152 def send(sender, recipients, msg): |
152 def send(sender, recipients, msg): |
153 try: |
153 try: |
154 return s.sendmail(sender, recipients, msg) |
154 return s.sendmail(sender, recipients, msg) |
155 except smtplib.SMTPRecipientsRefused as inst: |
155 except smtplib.SMTPRecipientsRefused as inst: |
156 recipients = [r[1] for r in inst.recipients.values()] |
156 recipients = [r[1] for r in inst.recipients.values()] |
157 raise error.Abort('\n' + '\n'.join(recipients)) |
157 raise error.Abort(b'\n' + b'\n'.join(recipients)) |
158 except smtplib.SMTPException as inst: |
158 except smtplib.SMTPException as inst: |
159 raise error.Abort(inst) |
159 raise error.Abort(inst) |
160 |
160 |
161 return send |
161 return send |
162 |
162 |
163 |
163 |
164 def _sendmail(ui, sender, recipients, msg): |
164 def _sendmail(ui, sender, recipients, msg): |
165 '''send mail using sendmail.''' |
165 '''send mail using sendmail.''' |
166 program = ui.config('email', 'method') |
166 program = ui.config(b'email', b'method') |
167 stremail = lambda x: ( |
167 stremail = lambda x: ( |
168 procutil.quote(stringutil.email(encoding.strtolocal(x))) |
168 procutil.quote(stringutil.email(encoding.strtolocal(x))) |
169 ) |
169 ) |
170 cmdline = '%s -f %s %s' % ( |
170 cmdline = b'%s -f %s %s' % ( |
171 program, |
171 program, |
172 stremail(sender), |
172 stremail(sender), |
173 ' '.join(map(stremail, recipients)), |
173 b' '.join(map(stremail, recipients)), |
174 ) |
174 ) |
175 ui.note(_('sending mail: %s\n') % cmdline) |
175 ui.note(_(b'sending mail: %s\n') % cmdline) |
176 fp = procutil.popen(cmdline, 'wb') |
176 fp = procutil.popen(cmdline, b'wb') |
177 fp.write(util.tonativeeol(msg)) |
177 fp.write(util.tonativeeol(msg)) |
178 ret = fp.close() |
178 ret = fp.close() |
179 if ret: |
179 if ret: |
180 raise error.Abort( |
180 raise error.Abort( |
181 '%s %s' |
181 b'%s %s' |
182 % ( |
182 % ( |
183 os.path.basename(program.split(None, 1)[0]), |
183 os.path.basename(program.split(None, 1)[0]), |
184 procutil.explainexit(ret), |
184 procutil.explainexit(ret), |
185 ) |
185 ) |
186 ) |
186 ) |
187 |
187 |
188 |
188 |
189 def _mbox(mbox, sender, recipients, msg): |
189 def _mbox(mbox, sender, recipients, msg): |
190 '''write mails to mbox''' |
190 '''write mails to mbox''' |
191 fp = open(mbox, 'ab+') |
191 fp = open(mbox, b'ab+') |
192 # Should be time.asctime(), but Windows prints 2-characters day |
192 # Should be time.asctime(), but Windows prints 2-characters day |
193 # of month instead of one. Make them print the same thing. |
193 # of month instead of one. Make them print the same thing. |
194 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime()) |
194 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime()) |
195 fp.write( |
195 fp.write( |
196 'From %s %s\n' |
196 b'From %s %s\n' |
197 % (encoding.strtolocal(sender), encoding.strtolocal(date)) |
197 % (encoding.strtolocal(sender), encoding.strtolocal(date)) |
198 ) |
198 ) |
199 fp.write(msg) |
199 fp.write(msg) |
200 fp.write('\n\n') |
200 fp.write(b'\n\n') |
201 fp.close() |
201 fp.close() |
202 |
202 |
203 |
203 |
204 def connect(ui, mbox=None): |
204 def connect(ui, mbox=None): |
205 '''make a mail connection. return a function to send mail. |
205 '''make a mail connection. return a function to send mail. |
206 call as sendmail(sender, list-of-recipients, msg).''' |
206 call as sendmail(sender, list-of-recipients, msg).''' |
207 if mbox: |
207 if mbox: |
208 open(mbox, 'wb').close() |
208 open(mbox, b'wb').close() |
209 return lambda s, r, m: _mbox(mbox, s, r, m) |
209 return lambda s, r, m: _mbox(mbox, s, r, m) |
210 if ui.config('email', 'method') == 'smtp': |
210 if ui.config(b'email', b'method') == b'smtp': |
211 return _smtp(ui) |
211 return _smtp(ui) |
212 return lambda s, r, m: _sendmail(ui, s, r, m) |
212 return lambda s, r, m: _sendmail(ui, s, r, m) |
213 |
213 |
214 |
214 |
215 def sendmail(ui, sender, recipients, msg, mbox=None): |
215 def sendmail(ui, sender, recipients, msg, mbox=None): |
217 return send(sender, recipients, msg) |
217 return send(sender, recipients, msg) |
218 |
218 |
219 |
219 |
220 def validateconfig(ui): |
220 def validateconfig(ui): |
221 '''determine if we have enough config data to try sending email.''' |
221 '''determine if we have enough config data to try sending email.''' |
222 method = ui.config('email', 'method') |
222 method = ui.config(b'email', b'method') |
223 if method == 'smtp': |
223 if method == b'smtp': |
224 if not ui.config('smtp', 'host'): |
224 if not ui.config(b'smtp', b'host'): |
225 raise error.Abort( |
225 raise error.Abort( |
226 _( |
226 _( |
227 'smtp specified as email transport, ' |
227 b'smtp specified as email transport, ' |
228 'but no smtp host configured' |
228 b'but no smtp host configured' |
229 ) |
229 ) |
230 ) |
230 ) |
231 else: |
231 else: |
232 if not procutil.findexe(method): |
232 if not procutil.findexe(method): |
233 raise error.Abort( |
233 raise error.Abort( |
234 _('%r specified as email transport, ' 'but not in PATH') |
234 _(b'%r specified as email transport, ' b'but not in PATH') |
235 % method |
235 % method |
236 ) |
236 ) |
237 |
237 |
238 |
238 |
239 def codec2iana(cs): |
239 def codec2iana(cs): |
240 '''''' |
240 '''''' |
241 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower()) |
241 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower()) |
242 |
242 |
243 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1" |
243 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1" |
244 if cs.startswith("iso") and not cs.startswith("iso-"): |
244 if cs.startswith(b"iso") and not cs.startswith(b"iso-"): |
245 return "iso-" + cs[3:] |
245 return b"iso-" + cs[3:] |
246 return cs |
246 return cs |
247 |
247 |
248 |
248 |
249 def mimetextpatch(s, subtype='plain', display=False): |
249 def mimetextpatch(s, subtype=b'plain', display=False): |
250 '''Return MIME message suitable for a patch. |
250 '''Return MIME message suitable for a patch. |
251 Charset will be detected by first trying to decode as us-ascii, then utf-8, |
251 Charset will be detected by first trying to decode as us-ascii, then utf-8, |
252 and finally the global encodings. If all those fail, fall back to |
252 and finally the global encodings. If all those fail, fall back to |
253 ISO-8859-1, an encoding with that allows all byte sequences. |
253 ISO-8859-1, an encoding with that allows all byte sequences. |
254 Transfer encodings will be used if necessary.''' |
254 Transfer encodings will be used if necessary.''' |
255 |
255 |
256 cs = ['us-ascii', 'utf-8', encoding.encoding, encoding.fallbackencoding] |
256 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding] |
257 if display: |
257 if display: |
258 cs = ['us-ascii'] |
258 cs = [b'us-ascii'] |
259 for charset in cs: |
259 for charset in cs: |
260 try: |
260 try: |
261 s.decode(pycompat.sysstr(charset)) |
261 s.decode(pycompat.sysstr(charset)) |
262 return mimetextqp(s, subtype, codec2iana(charset)) |
262 return mimetextqp(s, subtype, codec2iana(charset)) |
263 except UnicodeDecodeError: |
263 except UnicodeDecodeError: |
264 pass |
264 pass |
265 |
265 |
266 return mimetextqp(s, subtype, "iso-8859-1") |
266 return mimetextqp(s, subtype, b"iso-8859-1") |
267 |
267 |
268 |
268 |
269 def mimetextqp(body, subtype, charset): |
269 def mimetextqp(body, subtype, charset): |
270 '''Return MIME message. |
270 '''Return MIME message. |
271 Quoted-printable transfer encoding will be used if necessary. |
271 Quoted-printable transfer encoding will be used if necessary. |
272 ''' |
272 ''' |
273 cs = email.charset.Charset(charset) |
273 cs = email.charset.Charset(charset) |
274 msg = email.message.Message() |
274 msg = email.message.Message() |
275 msg.set_type(pycompat.sysstr('text/' + subtype)) |
275 msg.set_type(pycompat.sysstr(b'text/' + subtype)) |
276 |
276 |
277 for line in body.splitlines(): |
277 for line in body.splitlines(): |
278 if len(line) > 950: |
278 if len(line) > 950: |
279 cs.body_encoding = email.charset.QP |
279 cs.body_encoding = email.charset.QP |
280 break |
280 break |
359 |
359 |
360 def _addressencode(ui, name, addr, charsets=None): |
360 def _addressencode(ui, name, addr, charsets=None): |
361 assert isinstance(addr, bytes) |
361 assert isinstance(addr, bytes) |
362 name = headencode(ui, name, charsets) |
362 name = headencode(ui, name, charsets) |
363 try: |
363 try: |
364 acc, dom = addr.split('@') |
364 acc, dom = addr.split(b'@') |
365 acc.decode('ascii') |
365 acc.decode('ascii') |
366 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna') |
366 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna') |
367 addr = '%s@%s' % (acc, dom) |
367 addr = b'%s@%s' % (acc, dom) |
368 except UnicodeDecodeError: |
368 except UnicodeDecodeError: |
369 raise error.Abort(_('invalid email address: %s') % addr) |
369 raise error.Abort(_(b'invalid email address: %s') % addr) |
370 except ValueError: |
370 except ValueError: |
371 try: |
371 try: |
372 # too strict? |
372 # too strict? |
373 addr.decode('ascii') |
373 addr.decode('ascii') |
374 except UnicodeDecodeError: |
374 except UnicodeDecodeError: |
375 raise error.Abort(_('invalid local address: %s') % addr) |
375 raise error.Abort(_(b'invalid local address: %s') % addr) |
376 return pycompat.bytesurl( |
376 return pycompat.bytesurl( |
377 email.utils.formataddr((name, encoding.strfromlocal(addr))) |
377 email.utils.formataddr((name, encoding.strfromlocal(addr))) |
378 ) |
378 ) |
379 |
379 |
380 |
380 |
381 def addressencode(ui, address, charsets=None, display=False): |
381 def addressencode(ui, address, charsets=None, display=False): |
382 '''Turns address into RFC-2047 compliant header.''' |
382 '''Turns address into RFC-2047 compliant header.''' |
383 if display or not address: |
383 if display or not address: |
384 return address or '' |
384 return address or b'' |
385 name, addr = email.utils.parseaddr(encoding.strfromlocal(address)) |
385 name, addr = email.utils.parseaddr(encoding.strfromlocal(address)) |
386 return _addressencode(ui, name, encoding.strtolocal(addr), charsets) |
386 return _addressencode(ui, name, encoding.strtolocal(addr), charsets) |
387 |
387 |
388 |
388 |
389 def addrlistencode(ui, addrs, charsets=None, display=False): |
389 def addrlistencode(ui, addrs, charsets=None, display=False): |