Beispiel #1
0
def TestPop3Settings(session, settings, event):
    password = IndirectPassword(session.config, settings['password'])
    conn = _open_pop3_mailbox(session, event, settings['host'],
                              int(settings['port']), settings['username'],
                              password, settings.get('auth_type', 'password'),
                              settings['protocol'], True)
    if conn:
        conn.close()
        return True
    return False
Beispiel #2
0
 def open_mailbox(self, mbx_id, mfn):
     my_cfg = self.my_config
     if 'src:' in mfn[:5] and FormatMbxId(mbx_id) in my_cfg.mailbox:
         debug = ('pop3' in self.session.config.sys.debug) and 99 or 0
         password = IndirectPassword(self.session.config, my_cfg.password)
         return _open_pop3_mailbox(self.event,
                                   my_cfg.host,
                                   my_cfg.port,
                                   my_cfg.username,
                                   password,
                                   my_cfg.auth_type,
                                   my_cfg.protocol,
                                   debug,
                                   throw=POP3_IOError)
     return None
Beispiel #3
0
def _connect_imap(session,
                  settings,
                  event,
                  conn_cls=None,
                  timeout=30,
                  throw=False,
                  logged_in_cb=None,
                  source=None):
    def timed(*args, **kwargs):
        if source is not None:
            kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key, )
        return RunTimed(timeout, *args, **kwargs)

    def timed_imap(*args, **kwargs):
        if source is not None:
            kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key, )
        return _parse_imap(RunTimed(timeout, *args, **kwargs))

    conn = None
    try:
        # Prepare the data section of our event, for keeping state.
        for d in ('mailbox_state', ):
            if d not in event.data:
                event.data[d] = {}
        ev = event.data['connection'] = {
            'live': False,
            'error': [False, _('Nothing is wrong')]
        }

        # If we are given a conn class, use that - this allows mocks for
        # testing.
        if not conn_cls:
            req_stls = (settings.get('protocol') == 'imap_tls')
            want_ssl = (settings.get('protocol') == 'imap_ssl')
            conn_cls = IMAP4_SSL if want_ssl else IMAP4
        else:
            req_stls = want_ssl = False

        def mkconn():
            if want_ssl:
                need = [ConnBroker.OUTGOING_IMAPS]
            else:
                need = [ConnBroker.OUTGOING_IMAP]
            with ConnBroker.context(need=need):
                return conn_cls(settings.get('host'),
                                int(settings.get('port')))

        conn = timed(mkconn)
        if hasattr(conn, 'sock'):
            conn.sock.settimeout(120)
        conn.debug = ('imaplib' in session.config.sys.debug) and 4 or 0

        ok, data = timed_imap(conn.capability)
        if ok:
            capabilities = set(' '.join(data).upper().split())
        else:
            capabilities = set()

        if req_stls or ('STARTTLS' in capabilities and not want_ssl):
            try:
                ok, data = timed_imap(conn.starttls)
                if ok:
                    # Fetch capabilities again after STARTTLS
                    ok, data = timed_imap(conn.capability)
                    capabilities = set(' '.join(data).upper().split())
                    # Update the protocol to avoid getting downgraded later
                    if settings.get('protocol', '') != 'imap_ssl':
                        settings['protocol'] = 'imap_tls'
            except (IMAP4.error, IOError, socket.error):
                ok = False
            if not ok:
                ev['error'] = [
                    'tls',
                    _('Failed to make a secure TLS connection'),
                    '%s:%s' % (settings.get('host'), settings.get('port'))
                ]
                if throw:
                    raise throw(ev['error'][1])
                return WithaBool(False)

        username = password = ""
        try:
            error_type = 'auth'
            error_msg = _('Invalid username or password')
            username = settings.get('username', '').encode('utf-8')
            password = IndirectPassword(session.config,
                                        settings.get('password',
                                                     '')).encode('utf-8')

            if (settings.get('auth_type', '').lower() == 'oauth2'
                    and 'AUTH=XOAUTH2' in capabilities):
                error_type = 'oauth2'
                error_msg = _('Access denied by mail server')
                token_info = OAuth2.GetFreshTokenInfo(session, username)
                if not (username and token_info and token_info.access_token):
                    raise ValueError("Missing configuration")
                ok, data = timed_imap(
                    conn.authenticate, 'XOAUTH2',
                    lambda challenge: OAuth2.XOAuth2Response(
                        username, token_info))
                if not ok:
                    token_info.access_token = ''

            else:
                ok, data = timed_imap(conn.login, username, password)

        except (IMAP4.error, UnicodeDecodeError, ValueError):
            ok, data = False, None
        if not ok:
            auth_summary = ''
            if source is not None:
                auth_summary = source._summarize_auth()
            ev['error'] = [error_type, error_msg, username, auth_summary]
            if throw:
                raise throw(ev['error'][1])
            return WithaBool(False)

        if logged_in_cb is not None:
            logged_in_cb(conn, ev, capabilities)

        return conn

    except TimedOut:
        if 'imap' in session.config.sys.debug:
            session.ui.debug('%s' % traceback.format_exc())
        ev['error'] = ['timeout', _('Connection timed out')]
    except (ssl.CertificateError, ssl.SSLError):
        if 'imap' in session.config.sys.debug:
            session.ui.debug('%s' % traceback.format_exc())
        ev['error'] = [
            'tls',
            _('Failed to make a secure TLS connection'),
            '%s:%s' % (settings.get('host'), settings.get('port'))
        ]
    except (IMAP_IOError, IMAP4.error):
        if 'imap' in session.config.sys.debug:
            session.ui.debug('%s' % traceback.format_exc())
        ev['error'] = ['protocol', _('An IMAP protocol error occurred')]
    except (IOError, AttributeError, socket.error):
        if 'imap' in session.config.sys.debug:
            session.ui.debug('%s' % traceback.format_exc())
        ev['error'] = ['network', _('A network error occurred')]

    try:
        if conn:
            # Close the socket directly, in the hopes this will boot
            # any timed-out operations out of a hung state.
            conn.socket().shutdown(socket.SHUT_RDWR)
            conn.file.close()
    except (AttributeError, IOError, socket.error):
        pass
    if throw:
        raise throw(ev['error'])

    return None
Beispiel #4
0
def SendMail(session, msg_mid, from_to_msg_ev_tuples,
             test_only=False, test_route=None):
    routes = _RouteTuples(session, from_to_msg_ev_tuples,
                          test_route=test_route)

    # Randomize order of routes, so we don't always try the broken
    # one first. Any failure will bail out, but we do keep track of
    # our successes via. the event, so eventually everything sendable
    # should get sent.
    routes.sort(key=lambda k: random.randint(0, 10))

    # Update initial event state before we go through and start
    # trying to deliver stuff.
    for frm, route, to, msg, events in routes:
        for ev in (events or []):
            for rcpt in to:
                ev.private_data['>'.join([frm, rcpt])] = False

    for frm, route, to, msg, events in routes:
        for ev in events:
            ev.data['recipients'] = len(ev.private_data.keys())
            ev.data['delivered'] = len([k for k in ev.private_data
                                        if ev.private_data[k]])

    def mark(msg, events, log=True, clear_errors=False):
        for ev in events:
            ev.flags = Event.RUNNING
            ev.message = msg
            if clear_errors:
                if 'last_error' in ev.data:
                    del ev.data['last_error']
                if 'last_error_details' in ev.data:
                    del ev.data['last_error_details']
            if log:
                session.config.event_log.log_event(ev)
        session.ui.mark(msg)

    def fail(msg, events, details=None, exception=SendMailError):
        mark(msg, events, log=True)
        for ev in events:
            ev.data['last_error'] = msg
            if details:
                ev.data['last_error_details'] = details
        raise exception(msg, details=details)

    def smtp_do_or_die(msg, events, method, *args, **kwargs):
        rc, msg = method(*args, **kwargs)
        if rc != 250:
            fail(msg + ' (%s %s)' % (rc, msg), events,
                 details={'smtp_error': '%s: %s' % (rc, msg)})

    # Do the actual delivering...
    for frm, route, to, msg, events in routes:
        route_description = route['command'] or route['host']

        frm_vcard = session.config.vcards.get_vcard(frm)
        update_to_vcards = msg and msg["x-mp-internal-pubkeys-attached"]

        if 'sendmail' in session.config.sys.debug:
            sys.stderr.write(_('Sendmail: From %s (%s), to %s via %s\n')
                             % (frm, frm_vcard and frm_vcard.random_uid or '',
                                to, route_description))
        sm_write = sm_close = lambda: True

        mark(_('Sending via %s') % route_description, events, clear_errors=True)

        if route['command']:
            # Note: The .strip().split() here converts our cmd into a list,
            #       which should ensure that Popen does not spawn a shell
            #       with potentially exploitable arguments.
            cmd = (route['command'] % {"rcpt": ",".join(to)}).strip().split()
            proc = Popen(cmd, stdin=PIPE, long_running=True)
            sm_startup = None
            sm_write = proc.stdin.write

            def sm_close():
                proc.stdin.close()
                rv = proc.wait()
                if rv != 0:
                    fail(_('%s failed with exit code %d') % (cmd, rv), events,
                         details={'failed_command': cmd,
                                  'exit_code': rv})

            sm_cleanup = lambda: [proc.stdin.close(), proc.wait()]
            # FIXME: Update session UI with progress info
            for ev in events:
                ev.data['proto'] = 'subprocess'
                ev.data['command'] = cmd[0]

        elif route['protocol'] in ('smtp', 'smtorp', 'smtpssl', 'smtptls'):
            proto = route['protocol']
            host, port = route['host'], route['port']
            user = route['username']
            pwd = IndirectPassword(session.config, route['password'])
            auth_type = route['auth_type'] or ''
            smtp_ssl = proto in ('smtpssl', )  # FIXME: 'smtorp'

            for ev in events:
                ev.data['proto'] = proto
                ev.data['host'] = host
                ev.data['auth'] = bool(user and pwd)

            if 'sendmail' in session.config.sys.debug:
                sys.stderr.write(_('SMTP connection to: %s:%s as %s\n'
                                   ) % (host, port, user or '(anon)'))

            serverbox = [None]
            def sm_connect_server():
                server = (smtp_ssl and SMTP_SSL or SMTP
                          )(local_hostname='mailpile.local', timeout=25)
                if 'sendmail' in session.config.sys.debug:
                    server.set_debuglevel(1)
                if smtp_ssl or proto in ('smtorp', 'smtptls'):
                    conn_needs = [ConnBroker.OUTGOING_ENCRYPTED]
                else:
                    conn_needs = [ConnBroker.OUTGOING_SMTP]
                try:
                    with ConnBroker.context(need=conn_needs) as ctx:
                        server.connect(host, int(port))
                    server.ehlo_or_helo_if_needed()
                except (IOError, OSError, smtplib.SMTPServerDisconnected):
                    fail(_('Failed to connect to %s') % host, events,
                         details={'connection_error': True})

                return server

            def sm_startup():
                try:
                    server = sm_connect_server()
                    if not smtp_ssl:
                        # We always try to enable TLS, even if the user just
                        # requested plain-text smtp.  But we only throw errors
                        # if the user asked for encryption.
                        try:
                            server.starttls()
                            server.ehlo_or_helo_if_needed()
                        except:
                            if proto == 'smtptls':
                                raise
                            else:
                                server = sm_connect_server()
                except (ssl.CertificateError, ssl.SSLError):
                    fail(_('Failed to make a secure TLS connection'),
                         events,
                         details={
                             'tls_error': True,
                             'server': '%s:%d' % (host, port)},
                         exception=InsecureSmtpError)

                serverbox[0] = server

                if user:
                    try:
                        if auth_type.lower() == 'oauth2':
                            from mailpile.plugins.oauth import OAuth2
                            tok_info = OAuth2.GetFreshTokenInfo(session, user)
                            if not (user and tok_info and tok_info.access_token):
                                fail(_('Access denied by mail server'),
                                     events,
                                     details={'oauth_error': True,
                                              'username': user})
                            authstr = (OAuth2.XOAuth2Response(user, tok_info)
                                       ).encode('base64').replace('\n', '')
                            server.docmd('AUTH', 'XOAUTH2 ' + authstr)
                        else:
                            server.login(user.encode('utf-8'),
                                         (pwd or '').encode('utf-8'))
                    except UnicodeDecodeError:
                        fail(_('Bad character in username or password'),
                             events,
                             details={'authentication_error': True,
                                      'username': user})
                    except smtplib.SMTPAuthenticationError:
                        fail(_('Invalid username or password'), events,
                             details={'authentication_error': True,
                                      'username': user})
                    except smtplib.SMTPException:
                        # If the server does not support authentication, assume
                        # it's passwordless and try to carry one anyway.
                        pass

                smtp_do_or_die(_('Sender rejected by SMTP server'),
                               events, server.mail, frm)
                for rcpt in to:
                    rc, msg = server.rcpt(rcpt)
                    if (rc == SMTORP_HASHCASH_RCODE and
                            msg.startswith(SMTORP_HASHCASH_PREFIX)):
                        rc, msg = server.rcpt(SMTorP_HashCash(rcpt, msg))
                    if rc != 250:
                        fail(_('Server rejected recipient: %s') % rcpt, events)
                rcode, rmsg = server.docmd('DATA')
                if rcode != 354:
                    fail(_('Server rejected DATA: %s %s') % (rcode, rmsg))

            def sm_write(data):
                server = serverbox[0]
                for line in data.splitlines(True):
                    if line.startswith('.'):
                        server.send('.')
                    server.send(line)

            def sm_close():
                server = serverbox[0]
                server.send('\r\n.\r\n')
                smtp_do_or_die(_('Error spooling mail'),
                               events, server.getreply)

            def sm_cleanup():
                server = serverbox[0]
                if hasattr(server, 'sock'):
                    server.close()
        else:
            fail(_('Invalid route: %s') % route, events)

        try:
            # Run the entire connect/login sequence in a single timer, but
            # give it plenty of time in case the network is lame.
            if sm_startup:
                RunTimed(300, sm_startup)

            if test_only:
                return True

            mark(_('Preparing message…'), events)

            msg_string = MessageAsString(CleanMessage(session.config, msg))
            total = len(msg_string)
            while msg_string:
                if mailpile.util.QUITTING:
                    raise TimedOut(_('Quitting'))
                mark(('Sending message... (%d%%)'
                      ) % (100 * (total-len(msg_string))/total), events,
                     log=False)
                RunTimed(120, sm_write, msg_string[:20480])
                msg_string = msg_string[20480:]
            RunTimed(30, sm_close)

            mark(_n('Message sent, %d byte',
                    'Message sent, %d bytes',
                    total
                    ) % total, events)

            for ev in events:
                for rcpt in to:
                    vcard = session.config.vcards.get_vcard(rcpt)
                    if vcard:
                        vcard.record_history('send', time.time(), msg_mid)
                        if frm_vcard:
                            vcard.prefer_sender(rcpt, frm_vcard)
                        if update_to_vcards:
                            vcard.pgp_key_shared = int(time.time())
                        vcard.save()
                    ev.private_data['>'.join([frm, rcpt])] = True
                ev.data['bytes'] = total
                ev.data['delivered'] = len([k for k in ev.private_data
                                            if ev.private_data[k]])
        finally:
            sm_cleanup()
    return True