Beispiel #1
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):
        for ev in events:
            ev.flags = Event.RUNNING
            ev.message = msg
            if log:
                session.config.event_log.log_event(ev)
        session.ui.mark(msg)

    def fail(msg, events, details=None):
        mark(msg, events, log=True)
        for ev in events:
            ev.data['last_error'] = msg
        raise SendMailError(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)

        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, pwd = route['username'], route['password']
            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():
                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 InsecureSmtpError()
                        else:
                            server = sm_connect_server()
                serverbox[0] = server

                if user and pwd:
                    try:
                        server.login(user.encode('utf-8'), pwd.encode('utf-8'))
                    except UnicodeDecodeError:
                        fail(_('Bad character in username or password'),
                             events,
                             details={'authentication_error': True})
                    except smtplib.SMTPAuthenticationError:
                        fail(_('Invalid username or password'),
                             events,
                             details={'authentication_error': True})
                    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
Beispiel #2
0
def _real_startup(config):
    while config.http_worker is None:
        time.sleep(0.1)

    try:
        session_id = config.http_worker.httpd.make_session_id(None)
        mailpile.auth.SetLoggedIn(None,
                                  user='******',
                                  session_id=session_id)
        cookie = config.http_worker.httpd.session_cookie
        sspec = config.http_worker.httpd.sspec
        base_url = 'http://%s:%s%s' % sspec

        script_dir = os.path.dirname(os.path.realpath(__file__))
        script = os.path.join(script_dir, 'gui-o-matic.py')

        global __GUI__
        gui = __GUI__ = Popen(
            ['python', '-u', script],
            bufsize=1,  # line buffered
            stdin=PIPE,
            stderr=PIPE,
            long_running=True)
        stderr = []
        eater = threading.Thread(target=output_eater,
                                 args=[gui.stderr, stderr])
        eater.name = 'GUI(stderr)'
        eater.daemon = True
        eater.start()

        ico = lambda s: os.path.join(script_dir, 'icons-%(theme)s', s)
        gui.stdin.write(
            json.dumps({
                'app_name':
                'Mailpile',
                'indicator_icons': {
                    'startup': ico('startup.png'),
                    'normal': ico('normal.png'),
                    'working': ico('working.png'),
                    'attention': ico('attention.png'),
                    'shutdown': ico('shutdown.png')
                },
                'indicator_menu': [{
                    'label': _('Starting up ...'),
                    'item': 'status'
                }, {
                    'label': _('Open Mailpile'),
                    'item': 'open',
                    'op': 'show_url',
                    'args': [base_url]
                }, {
                    'label': _('Quit'),
                    'item': 'quit',
                    'op': 'get_url',
                    'args': [base_url + '/api/0/quitquitquit/']
                }],
                'http_cookies': {
                    base_url: [[cookie, session_id]]
                },
            }).strip() + '\nOK GO\n')

        indicator('set_menu_sensitive', item='quit')
        indicator('set_menu_sensitive', item='open')

        # FIXME: This sleep is lame
        time.sleep(5)
        if (gui.poll() is not None) or mailpile.util.QUITTING:
            return
    except:
        # If the basic indicator setup fails, we just assume it doesn't
        # work and go silently dead...
        return

    quitting = False
    try:
        # ...however, getting this far means if the indicator dies, then
        # the user tried to quit the app, so we should cooperate and die
        # (via the except below).

        while config.index is None or not config.tags:
            if mailpile.util.QUITTING:
                return
            if gui.poll() is not None:
                return
            time.sleep(1)
        indicator('set_status_normal')

        # FIXME: We should do more with the indicator... this is a bit lame.
        while True:
            if mailpile.util.QUITTING:
                quitting = True
                indicator('set_status_shutdown')
                indicator('set_menu_sensitive', item='open', sensitive=False)
                indicator('set_menu_sensitive', item='quit', sensitive=False)
                indicator('set_menu_label',
                          item='status',
                          label=_('Shutting down...'))
                l = threading.Lock()
                l.acquire()
                l.acquire()  # Deadlock, until app quits
            else:
                indicator('set_menu_label',
                          item='status',
                          label=_('%d messages') %
                          len(config.index and config.index.INDEX or []))
                time.sleep(1)

    except AttributeError:
        pass
    finally:
        try:
            if not quitting:
                Quit(Session(config)).run()
        except:
            pass
Beispiel #3
0
    def run(self,
            args=None, gpg_input=None, outputfd=None, partial_read_ok=False,
            send_passphrase=False, _raise=None):
        self.outputbuffers = dict([(x, []) for x in self.outputfds])
        self.threads = {}

        wtf = ' '.join(args)
        args = args[:] if args else []
        args.insert(0, self.gpgbinary)
        args.insert(1, "--utf8-strings")
        args.insert(1, "--with-colons")
        args.insert(1, "--verbose")
        args.insert(1, "--batch")
        args.insert(1, "--enable-progress-filter")
        if not self.use_agent:
            args.insert(1, "--no-use-agent")

        if self.homedir:
            args.insert(1, "--homedir=%s" % self.homedir)

        gpg_retcode = -1
        proc = None
        try:
            args.insert(1, "--status-fd=2")

            if self.passphrase and send_passphrase:
                if self.use_agent:
                    args.insert(1, "--no-use-agent")
                args.insert(2, "--passphrase-fd=0")

            if not self.passphrase and send_passphrase:
                self.debug('Running WITHOUT PASSPHRASE %s' % ' '.join(args))
                self.debug(traceback.format_stack())
            else:
                self.debug('Running %s' % ' '.join(args))

            # Here we go!
            proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0)

            # GnuPG is a bit crazy, and requires that the passphrase
            # be sent and the filehandle closed before anything else
            # interesting happens.
            if self.passphrase and send_passphrase:
                c = self.passphrase.read(BLOCKSIZE)
                while c != '':
                    proc.stdin.write(c)
                    c = self.passphrase.read(BLOCKSIZE)
                proc.stdin.write('\n')

            self.threads = {
                "stderr": StreamReader('gpgi-stderr(%s)' % wtf,
                                       proc.stderr, self.parse_stderr)
            }

            if outputfd:
                self.threads["stdout"] = StreamReader(
                    'gpgi-stdout-to-fd(%s)' % wtf,
                    proc.stdout, outputfd.write, lines=False)
            else:
                self.threads["stdout"] = StreamReader(
                    'gpgi-stdout-parsed(%s)' % wtf,
                    proc.stdout, self.parse_stdout)

            if gpg_input:
                # If we have output, we just stream it. Technically, this
                # doesn't really need to be a thread at the moment.
                self.debug('<<STDOUT<< %s' % gpg_input)
                StreamWriter('gpgi-output(%s)' % wtf,
                             proc.stdin, gpg_input,
                             partial_write_ok=partial_read_ok).join()
            else:
                proc.stdin.close()

            # Reap GnuPG
            gpg_retcode = proc.wait()

        finally:
            # Close this so GPG will terminate. This should already have
            # been done, but we're handling errors here...
            if proc and proc.stdin:
                proc.stdin.close()

        # Reap the threads
        self._reap_threads()

        if outputfd:
            outputfd.close()

        if gpg_retcode != 0 and _raise:
            raise _raise('GnuPG failed, exit code: %s' % gpg_retcode)

        return gpg_retcode, self.outputbuffers
Beispiel #4
0
    def run(self,
            args=None,
            gpg_input=None,
            outputfd=None,
            partial_read_ok=False,
            _raise=None):
        self.outputbuffers = dict([(x, []) for x in self.outputfds])
        self.pipes = {}
        self.threads = {}

        wtf = ' '.join(args)
        args = args[:] if args else []
        args.insert(0, self.gpgbinary)
        args.insert(1, "--utf8-strings")
        args.insert(1, "--with-colons")
        args.insert(1, "--verbose")
        args.insert(1, "--batch")
        args.insert(1, "--enable-progress-filter")
        if not self.use_agent:
            args.insert(1, "--no-use-agent")

        if self.homedir:
            args.insert(1, "--homedir=%s" % self.homedir)

        gpg_retcode = -1
        proc = status_pipe = passphrase_pipe = None
        popen_keeps_open = []
        try:
            status_pipe = Safe_Pipe()
            args.insert(1, "--status-fd=%d" % status_pipe.write_end.fileno())
            popen_keeps_open.append(status_pipe.write_end)

            if self.passphrase:
                passphrase_pipe = Safe_Pipe()
                if self.use_agent:
                    args.insert(1, "--no-use-agent")
                args.insert(
                    2,
                    "--passphrase-fd=%d" % passphrase_pipe.read_end.fileno())
                popen_keeps_open.append(passphrase_pipe.read_end)

            # Here we go!
            proc = Popen(args,
                         stdin=PIPE,
                         stdout=PIPE,
                         stderr=PIPE,
                         bufsize=0,
                         keep_open=popen_keeps_open)

            # GnuPG is a bit crazy, and requires that the passphrase
            # be sent and the filehandle closed before anything else
            # interesting happens.
            if self.passphrase:
                c = self.passphrase.read(BLOCKSIZE)
                while c != '':
                    passphrase_pipe.write(c)
                    c = self.passphrase.read(BLOCKSIZE)
                passphrase_pipe.write('\n')
                passphrase_pipe.write_end.close()

            self.threads = {
                "stderr":
                StreamReader('gpgi-stderr(%s)' % wtf, proc.stderr,
                             self.parse_stderr),
                "status":
                StreamReader('gpgi-status(%s)' % wtf, status_pipe.read_end,
                             self.parse_status),
            }

            if outputfd:
                self.threads["stdout"] = StreamReader('gpgi-stdout-to-fd(%s)' %
                                                      wtf,
                                                      proc.stdout,
                                                      outputfd.write,
                                                      lines=False)
            else:
                self.threads["stdout"] = StreamReader(
                    'gpgi-stdout-parsed(%s)' % wtf, proc.stdout,
                    self.parse_stdout)

            if gpg_input:
                # If we have output, we just stream it. Technically, this
                # doesn't really need to be a thread at the moment.
                StreamWriter('gpgi-output(%s)' % wtf,
                             proc.stdin,
                             gpg_input,
                             partial_write_ok=partial_read_ok).join()
            else:
                proc.stdin.close()

            # Reap GnuPG
            gpg_retcode = proc.wait()

        finally:
            # Here we close GPG's end of the status pipe, because
            # otherwise things may hang. We also close the passphrase pipe
            # at both ends, as it should be completely finished.
            if status_pipe:
                status_pipe.write_end.close()
            if passphrase_pipe:
                passphrase_pipe.close()

            # Close this so GPG will terminate. This should already have
            # been done, but we're handling errors here...
            if proc and proc.stdin:
                proc.stdin.close()

        # Reap the threads
        self._reap_threads()

        if outputfd:
            outputfd.close()

        if gpg_retcode != 0 and _raise:
            raise _raise('GnuPG failed, exit code: %s' % gpg_retcode)

        return gpg_retcode, self.outputbuffers
Beispiel #5
0
 def _popen(self, command, fd, long_running):
     self._reading = True
     proc = Popen(command, stdin=fd, stdout=PIPE, stderr=PIPE,
                           bufsize=0, long_running=long_running)
     return proc, proc.stdout