mercurial/mail.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43085 eef9a2d67051
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    42         smtplib.SMTP.__init__(self, **kwargs)
    42         smtplib.SMTP.__init__(self, **kwargs)
    43         self._ui = ui
    43         self._ui = ui
    44         self._host = host
    44         self._host = host
    45 
    45 
    46     def starttls(self, keyfile=None, certfile=None):
    46     def starttls(self, keyfile=None, certfile=None):
    47         if not self.has_extn("starttls"):
    47         if not self.has_extn(b"starttls"):
    48             msg = "STARTTLS extension not supported by server"
    48             msg = b"STARTTLS extension not supported by server"
    49             raise smtplib.SMTPException(msg)
    49             raise smtplib.SMTPException(msg)
    50         (resp, reply) = self.docmd("STARTTLS")
    50         (resp, reply) = self.docmd(b"STARTTLS")
    51         if resp == 220:
    51         if resp == 220:
    52             self.sock = sslutil.wrapsocket(
    52             self.sock = sslutil.wrapsocket(
    53                 self.sock,
    53                 self.sock,
    54                 keyfile,
    54                 keyfile,
    55                 certfile,
    55                 certfile,
    78         self.default_port = smtplib.SMTP_SSL_PORT
    78         self.default_port = smtplib.SMTP_SSL_PORT
    79         self._ui = ui
    79         self._ui = ui
    80 
    80 
    81     def _get_socket(self, host, port, timeout):
    81     def _get_socket(self, host, port, timeout):
    82         if self.debuglevel > 0:
    82         if self.debuglevel > 0:
    83             self._ui.debug('connect: %r\n' % ((host, port),))
    83             self._ui.debug(b'connect: %r\n' % ((host, port),))
    84         new_socket = socket.create_connection((host, port), timeout)
    84         new_socket = socket.create_connection((host, port), timeout)
    85         new_socket = sslutil.wrapsocket(
    85         new_socket = sslutil.wrapsocket(
    86             new_socket,
    86             new_socket,
    87             self.keyfile,
    87             self.keyfile,
    88             self.certfile,
    88             self.certfile,
   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
   291     return msg
   291     return msg
   292 
   292 
   293 
   293 
   294 def _charsets(ui):
   294 def _charsets(ui):
   295     '''Obtains charsets to send mail parts not containing patches.'''
   295     '''Obtains charsets to send mail parts not containing patches.'''
   296     charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
   296     charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
   297     fallbacks = [
   297     fallbacks = [
   298         encoding.fallbackencoding.lower(),
   298         encoding.fallbackencoding.lower(),
   299         encoding.encoding.lower(),
   299         encoding.encoding.lower(),
   300         'utf-8',
   300         b'utf-8',
   301     ]
   301     ]
   302     for cs in fallbacks:  # find unique charsets while keeping order
   302     for cs in fallbacks:  # find unique charsets while keeping order
   303         if cs not in charsets:
   303         if cs not in charsets:
   304             charsets.append(cs)
   304             charsets.append(cs)
   305     return [cs for cs in charsets if not cs.endswith('ascii')]
   305     return [cs for cs in charsets if not cs.endswith(b'ascii')]
   306 
   306 
   307 
   307 
   308 def _encode(ui, s, charsets):
   308 def _encode(ui, s, charsets):
   309     '''Returns (converted) string, charset tuple.
   309     '''Returns (converted) string, charset tuple.
   310     Finds out best charset by cycling through sendcharsets in descending
   310     Finds out best charset by cycling through sendcharsets in descending
   320             try:
   320             try:
   321                 return s.encode(pycompat.sysstr(ocs)), ocs
   321                 return s.encode(pycompat.sysstr(ocs)), ocs
   322             except UnicodeEncodeError:
   322             except UnicodeEncodeError:
   323                 pass
   323                 pass
   324             except LookupError:
   324             except LookupError:
   325                 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
   325                 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
   326         else:
   326         else:
   327             # Everything failed, ascii-armor what we've got and send it.
   327             # Everything failed, ascii-armor what we've got and send it.
   328             return s.encode('ascii', 'backslashreplace')
   328             return s.encode('ascii', 'backslashreplace')
   329     # We have a bytes of unknown encoding. We'll try and guess a valid
   329     # We have a bytes of unknown encoding. We'll try and guess a valid
   330     # encoding, falling back to pretending we had ascii even though we
   330     # encoding, falling back to pretending we had ascii even though we
   341                 try:
   341                 try:
   342                     return u.encode(pycompat.sysstr(ocs)), ocs
   342                     return u.encode(pycompat.sysstr(ocs)), ocs
   343                 except UnicodeEncodeError:
   343                 except UnicodeEncodeError:
   344                     pass
   344                     pass
   345                 except LookupError:
   345                 except LookupError:
   346                     ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
   346                     ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
   347     # if ascii, or all conversion attempts fail, send (broken) ascii
   347     # if ascii, or all conversion attempts fail, send (broken) ascii
   348     return s, 'us-ascii'
   348     return s, b'us-ascii'
   349 
   349 
   350 
   350 
   351 def headencode(ui, s, charsets=None, display=False):
   351 def headencode(ui, s, charsets=None, display=False):
   352     '''Returns RFC-2047 compliant header from given string.'''
   352     '''Returns RFC-2047 compliant header from given string.'''
   353     if not display:
   353     if not display:
   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):
   406 
   406 
   407 
   407 
   408 def mimeencode(ui, s, charsets=None, display=False):
   408 def mimeencode(ui, s, charsets=None, display=False):
   409     '''creates mime text object, encodes it if needed, and sets
   409     '''creates mime text object, encodes it if needed, and sets
   410     charset and transfer-encoding accordingly.'''
   410     charset and transfer-encoding accordingly.'''
   411     cs = 'us-ascii'
   411     cs = b'us-ascii'
   412     if not display:
   412     if not display:
   413         s, cs = _encode(ui, s, charsets)
   413         s, cs = _encode(ui, s, charsets)
   414     return mimetextqp(s, 'plain', cs)
   414     return mimetextqp(s, b'plain', cs)
   415 
   415 
   416 
   416 
   417 if pycompat.ispy3:
   417 if pycompat.ispy3:
   418 
   418 
   419     def parse(fp):
   419     def parse(fp):