Exemple #1
0
def HeaderPrints(message):
    """Generate fingerprints from message headers which identifies the MUA."""
    m = HeaderPrintMTADetails(message)
    u = HeaderPrintMUADetails(message, mta=m)[:20]
    g = HeaderPrintGenericDetails(message)[:50]
    mua = (u[1] if u else None)
    if mua and mua.startswith('Mozilla '):
        mua = mua.split()[-1]
    return {
        # The sender-ID headerprints includes MTA info
        'sender': md5_hex('\n'.join(m+u+g)),
        # Tool-chain headerprints ignore the MTA details
        'tools': md5_hex('\n'.join(u+g)),
        # Our best guess about what the MUA actually is; may be None
        'mua': mua}
Exemple #2
0
def HeaderPrints(message):
    """Generate fingerprints from message headers which identifies the MUA."""
    m = HeaderPrintMTADetails(message)
    u = HeaderPrintMUADetails(message, mta=m)[:20]
    g = HeaderPrintGenericDetails(message)[:50]
    mua = (u[1] if u else None)
    if mua and mua.startswith('Mozilla '):
        mua = mua.split()[-1]
    return {
        # The sender-ID headerprints includes MTA info
        'sender': md5_hex('\n'.join(m+u+g)),
        # Tool-chain headerprints ignore the MTA details
        'tools': md5_hex('\n'.join(u+g)),
        # Our best guess about what the MUA actually is; may be None
        'mua': mua}
Exemple #3
0
    def command(self):
        if "data" in self.data:
            data = self.data["data"][0]
        else:
            data = "".join(self.args)

        for gross in self.session.config.sys.md5sum_blacklist.split():
            if gross in data or not data:
                return self._error(_("I refuse to work with empty " "or gross data"), info={"data": data})

        return self._success(_("I hashed your data for you, yay!"), result=md5_hex(data))
Exemple #4
0
    def _show_avatar(self, protocol, host, email, size=60):

        if host == "localhost":
            default = protocol + "://" + host + "/static/img/avatar-default.png"
        else:
            default = "mm"

        digest = md5_hex(email.lower())
        gravatar_url = "https://www.gravatar.com/avatar/" + digest + "?"
        gravatar_url += urllib.urlencode({'d':default, 's':str(size)})

        return gravatar_url
Exemple #5
0
    def _show_avatar(self, protocol, host, email, size=60):

        if host == "localhost":
            default = protocol + "://" + host + "/static/img/avatar-default.png"
        else:
            default = "mm"

        digest = md5_hex(email.lower())
        gravatar_url = "https://www.gravatar.com/avatar/" + digest + "?"
        gravatar_url += urllib.urlencode({'d': default, 's': str(size)})

        return gravatar_url
Exemple #6
0
    def command(self):
        if 'data' in self.data:
            data = self.data['data']
        else:
            data = ''.join(self.args)

        if 'gross' in data or not data:
            return self._error(_('I refuse to work with empty or gross data'),
                               info={'data': data})

        return self._success(_('I hashed your data for you, yay!'),
                             result=md5_hex(data))
Exemple #7
0
    def command(self):
        if 'data' in self.data:
            data = self.data['data'][0]
        else:
            data = ''.join(self.args)

        for gross in self.session.config.sys.md5sum_blacklist.split():
            if gross in data or not data:
                return self._error(_('I refuse to work with empty '
                                     'or gross data'),
                                   info={'data': data})

        return self._success(_('I hashed your data for you, yay!'),
                             result=md5_hex(data))
Exemple #8
0
    def command(self):
        if 'data' in self.data:
            data = self.data['data']
        else:
            data = ''.join(self.args)

        for gross in self.session.config.sys.md5sum_blacklist.split():
            if gross in data or not data:
                return self._error(_('I refuse to work with empty '
                                     'or gross data'),
                                   info={'data': data})

        return self._success(_('I hashed your data for you, yay!'),
                             result=md5_hex(data))
Exemple #9
0
import re
import threading
import time
import traceback
from datetime import datetime
from tempfile import NamedTemporaryFile

from mailpile.i18n import gettext as _
from mailpile.i18n import ngettext as _n
from mailpile.crypto.gpgi import GPG_BINARY
from mailpile.safe_popen import Popen, PIPE
from mailpile.util import md5_hex, CryptoLock, safe_remove
from mailpile.util import sha512b64 as genkey


LEN_MD5 = len(md5_hex('testing'))
MD5_SUM_FORMAT = 'md5sum: %s'
MD5_SUM_PLACEHOLDER = MD5_SUM_FORMAT % ('0' * LEN_MD5)
MD5_SUM_RE = re.compile('(?m)^' + MD5_SUM_FORMAT % (r'[^\n]+',))

if sys.platform.startswith("win"):
    OPENSSL_COMMAND = 'OpenSSL\\bin\\openssl.exe'
    FILTER_MD5 = True
else:
    OPENSSL_COMMAND = "openssl"
    FILTER_MD5 = False


class IOFilter(threading.Thread):
    """
    This class will wrap a filehandle and spawn a background thread to
Exemple #10
0
import sys
import re
import threading
import traceback
from datetime import datetime
from tempfile import NamedTemporaryFile

from mailpile.i18n import gettext as _
from mailpile.i18n import ngettext as _n
from mailpile.crypto.gpgi import GPG_BINARY
from mailpile.safe_popen import Popen, PIPE
from mailpile.util import md5_hex, CryptoLock
from mailpile.util import sha512b64 as genkey


LEN_MD5 = len(md5_hex('testing'))
MD5_SUM_FORMAT = 'md5sum: %s'
MD5_SUM_PLACEHOLDER = MD5_SUM_FORMAT % ('0' * LEN_MD5)
MD5_SUM_RE = re.compile('(?m)^' + MD5_SUM_FORMAT % (r'[^\n]+',))

OPENSSL_COMMAND = "openssl"
if sys.platform.startswith("win"):
    OPENSSL_COMMAND = 'OpenSSL\\bin\\openssl.exe'

class IOFilter(threading.Thread):
    """
    This class will wrap a filehandle and spawn a background thread to
    filter either the input or output.
    """
    BLOCKSIZE = 8192
    def command(self):
        if self.data.get('_method', 'POST') != 'POST':
            # Allow HTTP GET as a no-op, so the user can see a friendly form.
            return self._success(_('Examine TLS certificates'))

        config = self.session.config
        tofu_save = self.data.get('tofu-save', '--tofu-save' in self.args)
        tofu_clear = self.data.get('tofu-clear', '--tofu-clear' in self.args)
        hosts = (list(s for s in self.args if not s.startswith('--')) +
                 self.data.get('host', []))

        def ts(t):
            return int(time.mktime(t.timetuple()))

        def oidName(oid):
            return {
                '2.5.4.3': 'commonName',
                '2.5.4.4': 'surname',
                '2.5.4.5': 'serialNumber',
                '2.5.4.6': 'countryName',
                '2.5.4.7': 'localityName',
                '2.5.4.8': 'stateOrProvinceName',
                '2.5.4.9': 'streetAddress',
                '2.5.4.10': 'organizationName',
                '2.5.4.11': 'organizationalUnitName'
                }.get(oid.dotted_string,
                      getattr(oid, '_name', oid.dotted_string))

        def oidmap(entries):
            return dict((oidName(e.oid), e.value) for e in entries)

        def subjmap(stext):
            def subjpair(kv):
                k, v = kv.split('=', 1)
                return ({'CN': 'commonName',
                         'C': 'countryName',
                         'ST': 'stateOrProvinceName',
                         'L': 'localityName',
                         'O': 'organizationName',
                         'OU': 'organizationalUnitName'}.get(k, k), v)
            parts = []
            for part in stext.strip().split('/'):
                if '=' in part:
                    parts.append(part)
                elif parts:
                    parts[-1] += '/' + part
            return dict(subjpair(kv) for kv in parts)

        def fingerprint(cert_sha_256):
            fp = ['%2.2x' % ord(b) for b in cert_sha_256]
            fp2 = [fp[i*2] + fp[i*2 + 1] for i in range(0, len(fp)/2)]
            return fp2

        def pts(t):
            dt, tz = t.rsplit(' ', 1)  # Strip off the timezone
            return datetime.datetime.strptime(dt, '%b %d %H:%M:%S %Y')

        def parse_pem_cert(cert_pem, s256):
            cert_sha_256 = s256.decode('base64')
            now = datetime.datetime.today()
            if cryptography_x509 is None:
                # Shell out to openssl, boo.
                (stdout, stderr) = subprocess.Popen(
                    ['openssl', 'x509',
                        '-subject', '-issuer', '-dates', '-noout'],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    stdin=subprocess.PIPE).communicate(input=str(cert_pem))
                if not stdout:
                    raise ValueError(stderr)
                details = dict(l.split('=', 1)
                               for l in stdout.strip().splitlines()
                               if l and '=' in l)
                details['notAfter'] = pts(details['notAfter'])
                details['notBefore'] = pts(details['notBefore'])
                return {
                    'fingerprint': fingerprint(cert_sha_256),
                    'date_matches': False,
                    'date_matches': ((details['notBefore'] < now) and
                                     (details['notAfter'] > now)),
                    'not_valid_before': ts(details['notBefore']),
                    'not_valid_after': ts(details['notAfter']),
                    'subject': subjmap(details['subject']),
                    'issuer': subjmap(details['issuer'])}
            else:
                parsed = cryptography_x509.load_pem_x509_certificate(
                    str(cert_pem),
                    cryptography.hazmat.backends.default_backend())
                return {
                    'fingerprint': fingerprint(cert_sha_256),
                    'date_matches': ((parsed.not_valid_before < now) and
                                     (parsed.not_valid_after > now)),
                    'not_valid_before': ts(parsed.not_valid_before),
                    'not_valid_after': ts(parsed.not_valid_after),
                    'subject': oidmap(parsed.subject),
                    'issuer': oidmap(parsed.issuer)}

        def attempt_starttls(addr, sock):
            # Attempt a minimal SMTP interaction, for STARTTLS support

            # We attempt a non-blocking peek unless we're sure this is
            # a port normally used for clear-text SMTP.
            peeking = int(addr[1]) not in (25, 587, 143)

            # If this isn't a known TLS port, then we sleep a bit to give a
            # greeting time to arrive.
            if peeking and int(addr[1]) not in (443, 465, 993, 995):
                time.sleep(0.4)

            try:
                # Look for an SMTP (or IMAP) greeting
                if peeking:
                    sock.setblocking(0)
                    # Note: This will throw a TypeError if we are connected
                    #       over Tor (or other SOCKS).
                    first = sock.recv(1024, socket.MSG_PEEK) or ''
                else:
                    sock.settimeout(10)
                    first = sock.recv(1024) or ''

                if first[:4] == '220 ':
                    # This is an SMTP greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('EHLO example.com\r\n')
                    if (sock.recv(1024) or '')[:1] == '2':
                        sock.sendall('STARTTLS\r\n')
                        sock.recv(1024)

                elif first[:4] == '* OK':
                    # This is an IMAP4 greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('* STARTTLS\r\n')
                    sock.recv(1024)

            except (TypeError, IOError, OSError):
                pass
            finally:
                sock.setblocking(1)

        certs = {}
        ok = changes = 0
        for host in hosts:
            try:
                addr = host.replace(' ', '').split(':') + ['443']
                addr = (addr[0], int(addr[1]))

                try:
                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,
                                              Master.OUTGOING_RAW]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=True, tofu=False)
                    hostname_matches = True
                    cert_validated = True

                except (ssl.SSLError, ssl.CertificateError) as e:
                    if isinstance(e, ssl.CertificateError):
                        cert_validated = True
                        hostname_matches = False
                    else:
                        cert_validated = False
                        hostname_matches = 'unknown'

                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,
                                              Master.OUTGOING_RAW]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=False, tofu=False)

                cert = ssls.getpeercert(True)
                s256 = tls_sock_cert_sha256(cert=cert)
                ssls.close()

                cfg_key = md5_hex('%s:%d' % addr)
                if tofu_clear:
                    if cfg_key in config.tls.keys():
                        del config.tls[cfg_key]
                        changes += 1
                if tofu_save:
                    if cfg_key not in config.tls.keys():
                        config.tls[cfg_key] = {'server': '%s:%d' % addr}
                    cert_tofu = config.tls[cfg_key]
                    cert_tofu.use_web_ca = False
                    cert_tofu.accept_certs.append(s256)
                    changes += 1
                else:
                    cert_tofu = config.tls.get(cfg_key, {})

                tofu_seen = s256 in cert_tofu.get('accept_certs', [])
                using_tofu = not cert_tofu.get('use_web_ca', True)
                cert = {
                    'current_time': int(time.time()),
                    'cert_validated': cert_validated,
                    'hostname_matches': hostname_matches,
                    'tofu_seen': tofu_seen,
                    'using_tofu': using_tofu,
                    'tofu_invalid': (using_tofu and not tofu_seen),
                    'pem': ssl.DER_cert_to_PEM_cert(cert)}

                cert.update(parse_pem_cert(cert['pem'], s256))

                certs[host] = (True, s256, cert, None)
                ok += 1
            except Exception as e:
                certs[host] = (
                    False, _('Failed to fetch certificate'), unicode(e),
                    traceback.format_exc())

        if changes:
            self._background_save(config=True)

        if ok:
            return self._success(_('Downloaded TLS certificates'),
                                 result=certs)
        else:
            return self._error(_('Failed to download TLS certificates'),
                               result=certs)
Exemple #12
0
import re
import threading
import time
import traceback
from datetime import datetime
from tempfile import NamedTemporaryFile

from mailpile.i18n import gettext as _
from mailpile.i18n import ngettext as _n
from mailpile.crypto.gpgi import GPG_BINARY
from mailpile.safe_popen import Popen, PIPE
from mailpile.util import md5_hex, CryptoLock, safe_remove
from mailpile.util import sha512b64 as genkey


LEN_MD5 = len(md5_hex("testing"))
MD5_SUM_FORMAT = "md5sum: %s"
MD5_SUM_PLACEHOLDER = MD5_SUM_FORMAT % ("0" * LEN_MD5)
MD5_SUM_RE = re.compile("(?m)^" + MD5_SUM_FORMAT % (r"[^\n]+",))

if sys.platform.startswith("win"):
    OPENSSL_COMMAND = "OpenSSL\\bin\\openssl.exe"
    FILTER_MD5 = True
else:
    OPENSSL_COMMAND = "openssl"
    FILTER_MD5 = False


class IOFilter(threading.Thread):
    """
    This class will wrap a filehandle and spawn a background thread to
Exemple #13
0
    def command(self):
        if self.data.get('_method', 'POST') != 'POST':
            # Allow HTTP GET as a no-op, so the user can see a friendly form.
            return self._success(_('Examine TLS certificates'))

        config = self.session.config
        tofu_save = self.data.get('tofu-save', '--tofu-save' in self.args)
        tofu_clear = self.data.get('tofu-clear', '--tofu-clear' in self.args)
        hosts = (list(s for s in self.args if not s.startswith('--')) +
                 self.data.get('host', []))

        def ts(t):
            return int(time.mktime(t.timetuple()))

        def oidName(oid):
            return {
                '2.5.4.3': 'commonName',
                '2.5.4.4': 'surname',
                '2.5.4.5': 'serialNumber',
                '2.5.4.6': 'countryName',
                '2.5.4.7': 'localityName',
                '2.5.4.8': 'stateOrProvinceName',
                '2.5.4.9': 'streetAddress',
                '2.5.4.10': 'organizationName',
                '2.5.4.11': 'organizationalUnitName'
                }.get(oid.dotted_string,
                      getattr(oid, '_name', oid.dotted_string))

        def oidmap(entries):
            return dict((oidName(e.oid), e.value) for e in entries)

        def subjmap(stext):
            def subjpair(kv):
                k, v = kv.split('=', 1)
                return ({'CN': 'commonName',
                         'C': 'countryName',
                         'ST': 'stateOrProvinceName',
                         'L': 'localityName',
                         'O': 'organizationName',
                         'OU': 'organizationalUnitName'}.get(k, k), v)
            parts = []
            for part in stext.strip().split('/'):
                if '=' in part:
                    parts.append(part)
                elif parts:
                    parts[-1] += '/' + part
            return dict(subjpair(kv) for kv in parts)

        def fingerprint(cert_sha_256):
            fp = ['%2.2x' % ord(b) for b in cert_sha_256]
            fp2 = [fp[i*2] + fp[i*2 + 1] for i in range(0, len(fp)/2)]
            return fp2

        def pts(t):
            dt, tz = t.rsplit(' ', 1)  # Strip off the timezone
            return datetime.datetime.strptime(dt, '%b %d %H:%M:%S %Y')

        def parse_pem_cert(cert_pem, s256):
            cert_sha_256 = s256.decode('base64')
            now = datetime.datetime.today()
            if cryptography_x509 is None:
                # Shell out to openssl, boo.
                (stdout, stderr) = subprocess.Popen(
                    ['openssl', 'x509',
                        '-subject', '-issuer', '-dates', '-noout'],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    stdin=subprocess.PIPE).communicate(input=str(cert_pem))
                if not stdout:
                    raise ValueError(stderr)
                details = dict(l.split('=', 1)
                               for l in stdout.strip().splitlines()
                               if l and '=' in l)
                details['notAfter'] = pts(details['notAfter'])
                details['notBefore'] = pts(details['notBefore'])
                return {
                    'fingerprint': fingerprint(cert_sha_256),
                    'date_matches': False,
                    'date_matches': ((details['notBefore'] < now) and
                                     (details['notAfter'] > now)),
                    'not_valid_before': ts(details['notBefore']),
                    'not_valid_after': ts(details['notAfter']),
                    'subject': subjmap(details['subject']),
                    'issuer': subjmap(details['issuer'])}
            else:
                parsed = cryptography_x509.load_pem_x509_certificate(
                    str(cert_pem),
                    cryptography.hazmat.backends.default_backend())
                return {
                    'fingerprint': fingerprint(cert_sha_256),
                    'date_matches': ((parsed.not_valid_before < now) and
                                     (parsed.not_valid_after > now)),
                    'not_valid_before': ts(parsed.not_valid_before),
                    'not_valid_after': ts(parsed.not_valid_after),
                    'subject': oidmap(parsed.subject),
                    'issuer': oidmap(parsed.issuer)}

        def attempt_starttls(addr, sock):
            # Attempt a minimal SMTP interaction, for STARTTLS support

            # We attempt a non-blocking peek unless we're sure this is
            # a port normally used for clear-text SMTP.
            peeking = int(addr[1]) not in (25, 587, 143)

            # If this isn't a known TLS port, then we sleep a bit to give a
            # greeting time to arrive.
            if peeking and int(addr[1]) not in (443, 465, 993, 995):
                time.sleep(0.4)

            try:
                # Look for an SMTP (or IMAP) greeting
                if peeking:
                    sock.setblocking(0)
                    # Note: This will throw a TypeError if we are connected
                    #       over Tor (or other SOCKS).
                    first = sock.recv(1024, socket.MSG_PEEK) or ''
                else:
                    sock.settimeout(10)
                    first = sock.recv(1024) or ''

                if first[:4] == '220 ':
                    # This is an SMTP greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('EHLO example.com\r\n')
                    if (sock.recv(1024) or '')[:1] == '2':
                        sock.sendall('STARTTLS\r\n')
                        sock.recv(1024)

                elif first[:4] == '* OK':
                    # This is an IMAP4 greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('* STARTTLS\r\n')
                    sock.recv(1024)

            except (TypeError, IOError, OSError):
                pass
            finally:
                sock.setblocking(1)

        certs = {}
        ok = changes = 0
        for host in hosts:
            try:
                addr = host.replace(' ', '').split(':') + ['443']
                addr = (addr[0], int(addr[1]))

                try:
                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,
                                              Master.OUTGOING_RAW]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=True, tofu=False)
                    hostname_matches = True
                    cert_validated = True

                except (ssl.SSLError, ssl.CertificateError) as e:
                    if isinstance(e, ssl.CertificateError):
                        cert_validated = True
                        hostname_matches = False
                    else:
                        cert_validated = False
                        hostname_matches = 'unknown'

                    with Master.context(need=[Master.OUTGOING_ENCRYPTED,
                                              Master.OUTGOING_RAW]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=False, tofu=False)

                cert = ssls.getpeercert(True)
                s256 = tls_sock_cert_sha256(cert=cert)
                ssls.close()

                cfg_key = md5_hex('%s:%d' % addr)
                if tofu_clear:
                    if cfg_key in config.tls.keys():
                        del config.tls[cfg_key]
                        changes += 1
                if tofu_save:
                    if cfg_key not in config.tls.keys():
                        config.tls[cfg_key] = {'server': '%s:%d' % addr}
                    cert_tofu = config.tls[cfg_key]
                    cert_tofu.use_web_ca = False
                    cert_tofu.accept_certs.append(s256)
                    changes += 1
                else:
                    cert_tofu = config.tls.get(cfg_key, {})

                tofu_seen = s256 in cert_tofu.get('accept_certs', [])
                using_tofu = not cert_tofu.get('use_web_ca', True)
                cert = {
                    'current_time': int(time.time()),
                    'cert_validated': cert_validated,
                    'hostname_matches': hostname_matches,
                    'tofu_seen': tofu_seen,
                    'using_tofu': using_tofu,
                    'tofu_invalid': (using_tofu and not tofu_seen),
                    'pem': ssl.DER_cert_to_PEM_cert(cert)}

                cert.update(parse_pem_cert(cert['pem'], s256))

                certs[host] = (True, s256, cert, None)
                ok += 1
            except Exception as e:
                certs[host] = (
                    False, _('Failed to fetch certificate'), unicode(e),
                    traceback.format_exc())

        if changes:
            self._background_save(config=True)

        if ok:
            return self._success(_('Downloaded TLS certificates'),
                                 result=certs)
        else:
            return self._error(_('Failed to download TLS certificates'),
                               result=certs)
Exemple #14
0
    def command(self):
        if self.data.get('_method', 'POST') != 'POST':
            # Allow HTTP GET as a no-op, so the user can see a friendly form.
            return self._success(_('Examine TLS certificates'))

        config = self.session.config
        tofu_save = self.data.get('tofu-save', '--tofu-save' in self.args)
        tofu_clear = self.data.get('tofu-clear', '--tofu-clear' in self.args)
        hosts = (list(s for s in self.args if not s.startswith('--')) +
                 self.data.get('host', []))

        def ts(t):
            return int(time.mktime(t.timetuple()))

        def oidName(oid):
            return {
                '2.5.4.3': 'commonName',
                '2.5.4.4': 'surname',
                '2.5.4.5': 'serialNumber',
                '2.5.4.6': 'countryName',
                '2.5.4.7': 'localityName',
                '2.5.4.8': 'stateOrProvinceName',
                '2.5.4.9': 'streetAddress',
                '2.5.4.10': 'organizationName',
                '2.5.4.11': 'organizationalUnitName'
            }.get(oid.dotted_string, getattr(oid, '_name', oid.dotted_string))

        def oidmap(entries):
            return dict((oidName(e.oid), e.value) for e in entries)

        def fingerprint(pcert):
            sha256 = cryptography.hazmat.primitives.hashes.SHA256()
            fp = ['%2.2x' % ord(b) for b in pcert.fingerprint(sha256)]
            fp2 = [fp[i * 2] + fp[i * 2 + 1] for i in range(0, len(fp) / 2)]
            return fp2

        def attempt_starttls(addr, sock):
            # Attempt a minimal SMTP interaction, for STARTTLS support

            # We attempt a non-blocking peek unless we're sure this is
            # a port normally used for clear-text SMTP.
            peeking = 0 if (int(addr[1]) in (25, 587)) else socket.MSG_PEEK

            # If this isn't a known TLS port, then we sleep a bit to give a
            # greeting time to arrive.
            if peeking and int(addr[1]) not in (443, 465, 993, 995):
                time.sleep(0.4)

            try:
                # Look for an SMTP (or IMAP) greeting
                if peeking:
                    sock.setblocking(0)
                first = sock.recv(1024, peeking) or ''

                if first[:4] == '220 ':
                    # This is an SMTP greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('EHLO example.com\r\n')
                    if (sock.recv(1024) or '')[:1] == '2':
                        sock.sendall('STARTTLS\r\n')
                        sock.recv(1024)

                elif first[:4] == '* OK':
                    # This is an IMAP4 greeting
                    if peeking:
                        sock.setblocking(1)
                        sock.recv(1024)
                    sock.sendall('* STARTTLS\r\n')
                    sock.recv(1024)

            except (IOError, OSError):
                pass
            finally:
                sock.setblocking(1)

        certs = {}
        ok = changes = 0
        for host in hosts:
            try:
                addr = host.replace(' ', '').split(':') + ['443']
                addr = (addr[0], int(addr[1]))

                try:
                    with Master.context(need=[
                            Master.OUTGOING_ENCRYPTED, Master.OUTGOING_RAW
                    ]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=True, tofu=False)
                    hostname_matches = True
                    cert_validated = True

                except (ssl.SSLError, ssl.CertificateError) as e:
                    if isinstance(e, ssl.CertificateError):
                        cert_validated = True
                        hostname_matches = False
                    else:
                        cert_validated = False
                        hostname_matches = 'unknown'

                    with Master.context(need=[
                            Master.OUTGOING_ENCRYPTED, Master.OUTGOING_RAW
                    ]) as ctx:
                        sock = socket.create_connection(addr, timeout=30)
                    attempt_starttls(addr, sock)
                    ssls = ssl.wrap_socket(sock, use_web_ca=False, tofu=False)

                cert = ssls.getpeercert(True)
                s256 = tls_sock_cert_sha256(cert=cert)
                ssls.close()

                cfg_key = md5_hex('%s:%d' % addr)
                if tofu_clear:
                    if cfg_key in config.tls.keys():
                        del config.tls[cfg_key]
                        changes += 1
                if tofu_save:
                    if cfg_key not in config.tls.keys():
                        config.tls[cfg_key] = {'server': '%s:%d' % addr}
                    cert_tofu = config.tls[cfg_key]
                    cert_tofu.use_web_ca = False
                    cert_tofu.accept_certs.append(s256)
                    changes += 1
                else:
                    cert_tofu = config.tls.get(cfg_key, {})

                tofu_seen = s256 in cert_tofu.get('accept_certs', [])
                using_tofu = not cert_tofu.get('use_web_ca', True)
                cert = {
                    'current_time': int(time.time()),
                    'cert_validated': cert_validated,
                    'hostname_matches': hostname_matches,
                    'tofu_seen': tofu_seen,
                    'using_tofu': using_tofu,
                    'tofu_invalid': (using_tofu and not tofu_seen),
                    'pem': ssl.DER_cert_to_PEM_cert(cert)
                }

                if cryptography is not None:
                    now = datetime.datetime.today()
                    parsed = cryptography.x509.load_pem_x509_certificate(
                        str(cert['pem']),
                        cryptography.hazmat.backends.default_backend())
                    cert.update({
                        'fingerprint':
                        fingerprint(parsed),
                        'date_matches': ((parsed.not_valid_before < now)
                                         and (parsed.not_valid_after > now)),
                        'not_valid_before':
                        ts(parsed.not_valid_before),
                        'not_valid_after':
                        ts(parsed.not_valid_after),
                        'subject':
                        oidmap(parsed.subject),
                        'issuer':
                        oidmap(parsed.issuer)
                    })

                certs[host] = (True, s256, cert, None)
                ok += 1
            except Exception as e:
                certs[host] = (False, _('Failed to fetch certificate'),
                               unicode(e), traceback.format_exc())

        if changes:
            self._background_save(config=True)

        if ok:
            return self._success(_('Downloaded TLS certificates'),
                                 result=certs)
        else:
            return self._error(_('Failed to download TLS certificates'),
                               result=certs)