# mail.py - mail sending bits for mercurial # # Copyright 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. from i18n import _ import os, smtplib, socket import email.Header, email.MIMEText, email.Utils import util def _smtp(ui): '''build an smtp connection and return a function to send mail''' local_hostname = ui.config('smtp', 'local_hostname') s = smtplib.SMTP(local_hostname=local_hostname) mailhost = ui.config('smtp', 'host') if not mailhost: raise util.Abort(_('no [smtp]host in hgrc - cannot send mail')) mailport = int(ui.config('smtp', 'port', 25)) ui.note(_('sending mail: smtp host %s, port %s\n') % (mailhost, mailport)) s.connect(host=mailhost, port=mailport) if ui.configbool('smtp', 'tls'): if not hasattr(socket, 'ssl'): raise util.Abort(_("can't use TLS: Python SSL support " "not installed")) ui.note(_('(using tls)\n')) s.ehlo() s.starttls() s.ehlo() username = ui.config('smtp', 'username') password = ui.config('smtp', 'password') if username and not password: password = ui.getpass() if username and password: ui.note(_('(authenticating to mail server as %s)\n') % (username)) s.login(username, password) def send(sender, recipients, msg): try: return s.sendmail(sender, recipients, msg) except smtplib.SMTPRecipientsRefused, inst: recipients = [r[1] for r in inst.recipients.values()] raise util.Abort('\n' + '\n'.join(recipients)) except smtplib.SMTPException, inst: raise util.Abort(inst) return send def _sendmail(ui, sender, recipients, msg): '''send mail using sendmail.''' program = ui.config('email', 'method') cmdline = '%s -f %s %s' % (program, util.email(sender), ' '.join(map(util.email, recipients))) ui.note(_('sending mail: %s\n') % cmdline) fp = util.popen(cmdline, 'w') fp.write(msg) ret = fp.close() if ret: raise util.Abort('%s %s' % ( os.path.basename(program.split(None, 1)[0]), util.explain_exit(ret)[0])) def connect(ui): '''make a mail connection. return a function to send mail. call as sendmail(sender, list-of-recipients, msg).''' if ui.config('email', 'method', 'smtp') == 'smtp': return _smtp(ui) return lambda s, r, m: _sendmail(ui, s, r, m) def sendmail(ui, sender, recipients, msg): send = connect(ui) return send(sender, recipients, msg) def validateconfig(ui): '''determine if we have enough config data to try sending email.''' method = ui.config('email', 'method', 'smtp') if method == 'smtp': if not ui.config('smtp', 'host'): raise util.Abort(_('smtp specified as email transport, ' 'but no smtp host configured')) else: if not util.find_exe(method): raise util.Abort(_('%r specified as email transport, ' 'but not in PATH') % method) def mimetextpatch(s, subtype='plain', display=False): '''If patch in utf-8 transfer-encode it.''' if not display: for cs in ('us-ascii', 'utf-8'): try: s.decode(cs) return email.MIMEText.MIMEText(s, subtype, cs) except UnicodeDecodeError: pass return email.MIMEText.MIMEText(s, subtype) def _charsets(ui): '''Obtains charsets to send mail parts not containing patches.''' charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')] fallbacks = [util._fallbackencoding.lower(), util._encoding.lower(), 'utf-8'] for cs in fallbacks: # util.unique does not keep order if cs not in charsets: charsets.append(cs) return [cs for cs in charsets if not cs.endswith('ascii')] def _encode(ui, s, charsets): '''Returns (converted) string, charset tuple. Finds out best charset by cycling through sendcharsets in descending order. Tries both _encoding and _fallbackencoding for input. Only as last resort send as is in fake ascii. Caveat: Do not use for mail parts containing patches!''' try: s.decode('ascii') except UnicodeDecodeError: sendcharsets = charsets or _charsets(ui) for ics in (util._encoding, util._fallbackencoding): try: u = s.decode(ics) except UnicodeDecodeError: continue for ocs in sendcharsets: try: return u.encode(ocs), ocs except UnicodeEncodeError: pass except LookupError: ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs) # if ascii, or all conversion attempts fail, send (broken) ascii return s, 'us-ascii' def headencode(ui, s, charsets=None, display=False): '''Returns RFC-2047 compliant header from given string.''' if not display: # split into words? s, cs = _encode(ui, s, charsets) return str(email.Header.Header(s, cs)) return s def addressencode(ui, address, charsets=None, display=False): '''Turns address into RFC-2047 compliant header.''' if display or not address: return address or '' name, addr = email.Utils.parseaddr(address) name = headencode(ui, name, charsets) try: acc, dom = addr.split('@') acc = acc.encode('ascii') dom = dom.encode('idna') addr = '%s@%s' % (acc, dom) except UnicodeDecodeError: raise util.Abort(_('invalid email address: %s') % addr) except ValueError: try: # too strict? addr = addr.encode('ascii') except UnicodeDecodeError: raise util.Abort(_('invalid local address: %s') % addr) return email.Utils.formataddr((name, addr)) def mimeencode(ui, s, charsets=None, display=False): '''creates mime text object, encodes it if needed, and sets charset and transfer-encoding accordingly.''' cs = 'us-ascii' if not display: s, cs = _encode(ui, s, charsets) return email.MIMEText.MIMEText(s, 'plain', cs)