Beispiel #1
0
    def command(self):
        if (self.session.config.sys.lockdown or 0) > 1:
            return self._error(_('In lockdown, doing nothing.'))

        if 'mid' in self.data:
            return self._error(_('Please use update for editing messages'))

        session, idx = self.session, self._idx()
        ephemeral = (self.args and "ephemeral" in self.args)
        cid = self.data.get('cid', [None])[0]

        email, ephemeral = self.CreateMessage(idx, session, self._new_msgid(),
                                              cid=cid,
                                              ephemeral=ephemeral)
        if not ephemeral:
            self._tag_blank([email])

        email_updates = self._get_email_updates(idx,
                                                emails=[email],
                                                create=True)
        update_string = email_updates and email_updates[0][1]
        if update_string:
            email.update_from_string(session, update_string)

        return self._edit_messages([email],
                                   ephemeral=ephemeral,
                                   new=(ephemeral or not update_string))
Beispiel #2
0
 def __setitem__(self, key, value):
     key = self.__fixkey__(key)
     checker = self.get_rule(key)[self.RULE_CHECKER]
     if not checker is True:
         if checker is False:
             if isinstance(value, dict) and isinstance(self[key], dict):
                 for k, v in value.iteritems():
                     self[key][k] = v
                 return
             raise ConfigValueError(_('Modifying %s/%s is not '
                                      'allowed') % (self._name, key))
         elif isinstance(checker, (list, set, tuple)):
             if value not in checker:
                 raise ConfigValueError(_('Invalid value for %s/%s: %s'
                                          ) % (self._name, key, value))
         elif isinstance(checker, (type, type(RuledContainer))):
             try:
                 if value is None:
                     value = checker()
                 else:
                     value = checker(value)
             except (ConfigValueError):
                 raise
             except (validators.IgnoreValue):
                 return
             except (ValueError, TypeError):
                 raise ValueError(_('Invalid value for %s/%s: %s'
                                    ) % (self._name, key, value))
         else:
             raise Exception(_('Unknown constraint for %s/%s: %s'
                               ) % (self._name, key, checker))
     self.__passkey__(key, value)
     self.__createkey_and_setitem__(key, value)
     self.__passkey_recurse__(key, value)
Beispiel #3
0
    def command(self):
        session, config, idx = self.session, self.session.config, self._idx()
        args = list(self.args)

        # On the CLI, anything after -- is the new metadata subject.
        if '--' in args:
            subject = ' '.join(args[(args.index('--')+1):])
            args = args[:args.index('--')]
        else:
            subject = self.data.get('subject', [None])[0]

        # Message IDs can come from post data
        for mid in self.data.get('mid', []):
            args.append('=%s' % mid)
        emails = [self._actualize_ephemeral(i) for i in
                  self._choose_messages(args, allow_ephemeral=True)]

        if emails:
            if self.data.get('_method', 'POST') == 'POST':
                for email in emails:
                    idx.unthread_message(email.msg_mid(), new_subject=subject)
                self._background_save(index=True)
                return self._return_search_results(
                    _('Unthreaded %d messages') % len(emails), emails)
            else:
                return self._return_search_results(
                    _('Unthread %d messages') % len(emails), emails)
        else:
            return self._error(_('Nothing to do!'))
Beispiel #4
0
    def command(self, save=True, auto=False):
        res = {
            'api_methods': [],
            'javascript_classes': [],
            'css_files': []
        }
        if self.args:
            # Short-circuit if we're serving templates...
            return self._success(_('Serving up API content'), result=res)

        session, config = self.session, self.session.config
        urlmap = UrlMap(session)
        for method in ('GET', 'POST', 'UPDATE', 'DELETE'):
            for cmd in urlmap._api_commands(method, strict=True):
                cmdinfo = {
                    "url": cmd.SYNOPSIS[2],
                    "method": method
                }
                if hasattr(cmd, 'HTTP_QUERY_VARS'):
                    cmdinfo["query_vars"] = cmd.HTTP_QUERY_VARS
                if hasattr(cmd, 'HTTP_POST_VARS'):
                    cmdinfo["post_vars"] = cmd.HTTP_POST_VARS
                if hasattr(cmd, 'HTTP_OPTIONAL_VARS'):
                    cmdinfo["optional_vars"] = cmd.OPTIONAL_VARS
                res['api_methods'].append(cmdinfo)

        created_js = []
        for cls, filename in sorted(list(
                config.plugins.get_js_classes().iteritems())):
            try:
                parts = cls.split('.')[:-1]
                for i in range(1, len(parts)):
                    parent = '.'.join(parts[:i+1])
                    if parent not in created_js:
                        res['javascript_classes'].append({
                            'classname': parent,
                            'code': ''
                        })
                        created_js.append(parent)
                with open(filename, 'rb') as fd:
                    res['javascript_classes'].append({
                        'classname': cls,
                        'code': fd.read().decode('utf-8')
                    })
                    created_js.append(cls)
            except (OSError, IOError, UnicodeDecodeError):
                self._ignore_exception()

        for cls, filename in sorted(list(
                config.plugins.get_css_files().iteritems())):
            try:
                with open(filename, 'rb') as fd:
                    res['css_files'].append({
                        'classname': cls,
                        'css': fd.read().decode('utf-8')
                    })
            except (OSError, IOError, UnicodeDecodeError):
                self._ignore_exception()

        return self._success(_('Generated Javascript API'), result=res)
Beispiel #5
0
    def setup_command(self, session):
        # FIXME: Implement and add more potential sources of e-mails
        macmail_emails = self._testing_data(self._get_macmail_emails, ['1'])
        tbird_emails = self._testing_data(self._get_tbird_emails, ['1'])
        gnupg_emails = self._testing_data(self._get_gnupg_emails, [
            {
                'name': 'Innocent Adventurer',
                'address': '*****@*****.**',
                'source': 'The Youtubes'
            },
            {
                'name': 'Chelsea Manning',
                'address': '*****@*****.**',
                'source': 'Internal Tribute Store'
            },
            {
                'name': 'MUSCULAR',
                'address': '*****@*****.**',
                'source': 'Well funded adversaries'
            }
        ])

        emails = macmail_emails + tbird_emails + gnupg_emails
        if not emails:
            return self._error(_('No e-mail addresses found'))
        else:
            return self._success(_('Discovered e-mail addresses'), {
                'emails': emails
            })
Beispiel #6
0
    def setup_command(self, session):
        config = session.config
        if self.data.get('_method') == 'POST' or self._testing():
            language = self.data.get('language', [''])[0]
            if language:
                try:
                    i18n = lambda: ActivateTranslation(session, config,
                                                       language)
                    if not self._testing_yes(i18n):
                        raise ValueError('Failed to configure i18n')
                    config.prefs.language = language
                    if not self._testing():
                        self._background_save(config=True)
                except ValueError:
                    return self._error(_('Invalid language: %s') % language)

            if not session.config.tags:
                # Intial configuration of app goes here
                SetupMagic.setup_command(self, session)

        results = {
            'languages': ListTranslations(config),
            'language': config.prefs.language
        }
        return self._success(_('Welcome to Mailpile!'), results)
Beispiel #7
0
        def _new_key_created(self, event, vcard_rid, passphrase):
            config = self.session.config
            fingerprint = self._key_generator.generated_key
            if fingerprint:
                vcard = vcard_rid and config.vcards.get_vcard(vcard_rid)
                if vcard:
                    vcard.pgp_key = fingerprint
                    vcard.save()
                    event.message = _('The PGP key for %s is ready for use.'
                                      ) % vcard.email
                else:
                    event.message = _('PGP key generation is complete')

                # Record the passphrase!
                config.secrets[fingerprint] = {'password': passphrase}

                # FIXME: Toggle something that indicates we need a backup ASAP.
                self._background_save(config=True)
            else:
                event.message = _('PGP key generation failed!')
                event.data['keygen_failed'] = True

            event.flags = event.COMPLETE
            event.data['keygen_finished'] = int(time.time())
            config.event_log.log_event(event)
Beispiel #8
0
    def command(self, create=True, outbox=False):
        session, config, idx = self.session, self.session.config, self._idx()
        email_updates = self._get_email_updates(idx,
                                                create=create,
                                                noneok=outbox)

        if not email_updates:
            return self._error(_('Nothing to do!'))
        try:
            if (self.data.get('file-data') or [''])[0]:
                if not Attach(session, data=self.data).command(emails=emails):
                    return self._error(_('Failed to attach files'))

            for email, update_string in email_updates:
                email.update_from_string(session, update_string, final=outbox)

            emails = [e for e, u in email_updates]
            message = _('%d message(s) updated') % len(email_updates)

            self._tag_blank(emails, untag=True)
            self._tag_drafts(emails, untag=outbox)
            self._tag_outbox(emails, untag=(not outbox))

            if outbox:
                self._create_contacts(emails)
                return self._return_search_results(message, emails,
                                                   sent=emails)
            else:
                return self._edit_messages(emails, new=False, tag=False)
        except KeyLookupError, kle:
            return self._error(_('Missing encryption keys'),
                               info={'missing_keys': kle.missing})
Beispiel #9
0
    def command(self):
        cfg, idx = self.session.config, self.session.config.index
        if not idx:
            return self._error(_('The index is not ready yet'))

        # Collect a list of messages from the outbox
        messages = []
        for tag in cfg.get_tags(type='outbox'):
            search = ['in:%s' % tag._key]
            for msg_idx_pos in idx.search(self.session, search,
                                          order='flat-index').as_set():
                messages.append('=%s' % b36(msg_idx_pos))

        # Messages no longer in the outbox get their events canceled...
        if cfg.event_log:
            events = cfg.event_log.incomplete(source='.plugins.compose.Sendit')
            for ev in events:
                if ('mid' in ev.data and
                        ('=%s' % ev.data['mid']) not in messages):
                    ev.flags = ev.COMPLETE
                    ev.message = _('Sending cancelled.')
                    cfg.event_log.log_event(ev)

        # Send all the mail!
        if messages:
            self.args = tuple(set(messages))
            return Sendit.command(self)
        else:
            return self._success(_('The outbox is empty'))
Beispiel #10
0
    def change_state(self):
        with self._lock:
            next_state = self._choose_state()
            if next_state != self._state:
                self._state(False)
                self._state = next_state
                self._state(True)
                return True
            else:
                label = None

                if self.config.index_loading:
                    pass  # new_mail_notifications handles this
                elif self._state in (self._state_need_setup,):
                    label =  _('Mailpile') + ': ' + _('New Installation')
                elif self._state not in (
                        self._state_logged_in, self._state_shutting_down):
                    label = _('Mailpile') + ': ' + _('Please log in')

                # FIXME: We rely on sending garbage over the socket
                #        regularly to check for errors. When that is
                #        gone we might not need to be so chatty.
                #        Until then: do not remove, it breaks shutdown!
                if label:
                    self._do('set_item', id="notification", label=label)

                return False
Beispiel #11
0
    def command(self, before_setup=True, after_setup=True):
        session = self.session
        err = cnt = 0

        migrations = []
        for a in self.args:
            if a in MIGRATIONS:
                migrations.append(MIGRATIONS[a])
            else:
                raise UsageError(_('Unknown migration: %s (available: %s)'
                                   ) % (a, ', '.join(MIGRATIONS.keys())))

        if not migrations:
            migrations = ((before_setup and MIGRATIONS_BEFORE_SETUP or []) +
                          (after_setup and MIGRATIONS_AFTER_SETUP or []))

        for mig in migrations:
            try:
                if mig(session):
                    cnt += 1
                else:
                    err += 1
            except:
                self._ignore_exception()
                err += 1

        self.session.config.version = APPVER  # We've migrated to this!

        self._background_save(config=True)
        return self._success(_('Performed %d migrations, failed %d.'
                               ) % (cnt, err))
Beispiel #12
0
 def _configure_sending_route(self, vcard, route_id):
     # Sending route
     route = self.session.config.routes.get(route_id)
     protocol = self.data.get('route-protocol', ['none'])[0]
     if protocol == 'none':
         if route:
             del self.session.config.routes[route_id]
         vcard.route = ''
         return
     elif protocol == 'local':
         route.password = route.username = route.host = ''
         route.name = _("Local mail")
         route.command = self.data.get('route-command', [None]
                                       )[0] or self._sendmail_command()
     elif protocol in ('smtp', 'smtptls', 'smtpssl'):
         route.command = ''
         route.name = vcard.email
         for var in ('route-username', 'route-password',
                     'route-auth_type', 'route-host', 'route-port'):
            rvar = var.split('-', 1)[1]
            route[rvar] = self.data.get(var, [''])[0]
     else:
         raise ValueError(_('Unhandled outgoing mail protocol: %s'
                            ) % protocol)
     route.protocol = protocol
     vcard.route = route_id
Beispiel #13
0
    def command(self):
        if (self.session.config.sys.lockdown or 0) > 1:
            return self._error(_('In lockdown, doing nothing.'))

        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        handle, line_ids = self.args[0], self.args[1:]
        vcard = config.vcards.get_vcard(handle)
        if not vcard:
            return self._error('%s not found: %s' % (self.VCARD, handle))
        config.vcards.deindex_vcard(vcard)
        removed = 0
        try:
            removed = vcard.remove(*[int(li) for li in line_ids])
            vcard.save()
            return self._success(_("Removed %d lines") % removed,
                result=self._vcard_list([vcard], simplify=True, info={
                    'updated': handle,
                    'removed': removed
                }))
        except KeyboardInterrupt:
            raise
        except:
            config.vcards.index_vcard(vcard)
            self._ignore_exception()
            return self._error(_('Error removing lines from %s') % handle)
        finally:
            config.vcards.index_vcard(vcard)
Beispiel #14
0
    def command(self):
        if (self.session.config.sys.lockdown or 0) > 1:
            return self._error(_('In lockdown, doing nothing.'))

        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        if self.args:
            handle, lines = self.args[0], self.args[1:]
        else:
            handle = self.data.get('rid', self.data.get('email', [None]))[0]
            if not handle:
                raise ValueError('Must set rid or email to choose VCard')
            name, value, replace, replace_all = (self.data.get(n, [None])[0]
                for n in ('name', 'value', 'replace', 'replace_all'))
            if not name or not value or ':' in name or '=' in name:
                raise ValueError('Must send a line name and line data')
            value = '%s:%s' % (name, value)
            if replace:
                value = '%d=%s' % (replace, value)
            elif replace_all:
                value = '=' + value
            lines = [value]

        vcard = config.vcards.get_vcard(handle)
        if not vcard:
            return self._error('%s not found: %s' % (self.VCARD, handle))
        config.vcards.deindex_vcard(vcard)
        client = self.data.get('client', [vcard.USER_CLIENT])[0]
        try:
            for l in lines:
                if l[0] == '=':
                    l = l[1:]
                    name = l.split(':', 1)[0]
                    existing = [ex._line_id for ex in vcard.get_all(name)]
                    vcard.add(VCardLine(l.strip()), client=client)
                    vcard.remove(*existing)

                elif '=' in l[:5]:
                    ln, l = l.split('=', 1)
                    vcard.set_line(int(ln.strip()), VCardLine(l.strip()),
                                   client=client)

                else:
                    vcard.add(VCardLine(l), client=client)

            vcard.save()
            return self._success(_("Added %d lines") % len(lines),
                result=self._vcard_list([vcard], simplify=True, info={
                    'updated': handle,
                    'added': len(lines)
                }))
        except KeyboardInterrupt:
            raise
        except:
            config.vcards.index_vcard(vcard)
            self._ignore_exception()
            return self._error(_('Error adding lines to %s') % handle)
        finally:
            config.vcards.index_vcard(vcard)
Beispiel #15
0
 def _fix_disable_key(self, fprint, comment=""):
     return [
         _("Disable bad keys:") + ("  " + comment if comment else ""),
         _("Run: %s") % ("`gpg --edit-key %s`" % fprint),
         _("Type %s") % "`disable`",
         _("Type %s") % "`save`",
     ]
Beispiel #16
0
    def command(self, format, terms=None, **kwargs):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        if not format in PluginManager.CONTACT_IMPORTERS.keys():
            session.ui.error("No such import format")
            return False

        importer = PluginManager.CONTACT_IMPORTERS[format]

        if not all([x in kwargs.keys() for x in importer.required_parameters]):
            session.ui.error(
                _("Required paramter missing. Required parameters "
                  "are: %s") % ", ".join(importer.required_parameters))
            return False

        allparams = importer.required_parameters + importer.optional_parameters

        if not all([x in allparams for x in kwargs.keys()]):
            session.ui.error(
                _("Unknown parameter passed to importer. "
                  "Provided %s; but known parameters are: %s"
                  ) % (", ".join(kwargs), ", ".join(allparams)))
            return False

        imp = importer(kwargs)
        if terms:
            contacts = imp.filter_contacts(terms)
        else:
            contacts = imp.get_contacts()

        for importedcontact in contacts:
            # Check if contact exists. If yes, then update. Else create.
            pass
Beispiel #17
0
            def sm_startup():
                if 'sendmail' in session.config.sys.debug:
                    server.set_debuglevel(1)
                if proto == 'smtorp':
                    server.connect(host, int(port),
                                   socket_cls=session.config.get_tor_socket())
                else:
                    server.connect(host, int(port))
                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()
                    except:
                        if sendmail.startswith('smtptls'):
                            raise InsecureSmtpError()
                if user and pwd:
                    try:
                        server.login(user, pwd)
                    except smtplib.SMTPAuthenticationError:
                        fail(_('Invalid username or password'), events)

                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 recpient: %s') % rcpt, events)
                rcode, rmsg = server.docmd('DATA')
                if rcode != 354:
                    fail(_('Server rejected DATA: %s %s') % (rcode, rmsg))
Beispiel #18
0
 def render_web(self, cfg, tpl_names, data):
     """Render data as HTML"""
     alldata = default_dict(self.html_variables)
     alldata['config'] = cfg
     alldata.update(data)
     try:
         template = self._web_template(cfg, tpl_names)
         if template:
             return template.render(alldata)
         else:
             tpl_esc_names = [escape_html(tn) for tn in tpl_names]
             return self._render_error(cfg, {
                 'error': _('Template not found'),
                 'details': ' or '.join(tpl_esc_names),
                 'data': alldata
             })
     except (UndefinedError, ):
         tpl_esc_names = [escape_html(tn) for tn in tpl_names]
         return self._render_error(cfg, {
             'error': _('Template error'),
             'details': ' or '.join(tpl_esc_names),
             'traceback': traceback.format_exc(),
             'data': alldata
         })
     except (TemplateNotFound, TemplatesNotFound), e:
         tpl_esc_names = [escape_html(tn) for tn in tpl_names]
         return self._render_error(cfg, {
             'error': _('Template not found'),
             'details': 'In {0!s}:\n{1!s}'.format(e.name, e.message),
             'data': alldata
         })
Beispiel #19
0
    def auto_configure_tor(self, session, hostport=None):
        need_raw = [ConnBroker.OUTGOING_RAW]
        hostport = hostport or ('127.0.0.1', 9050)
        try:
            with ConnBroker.context(need=need_raw) as context:
                tor = socket.create_connection(hostport, timeout=10)
        except IOError:
            return  _('Failed to connect to Tor on %s:%s. Is it installed?'
                      ) % hostport

        # If that succeeded, we might have Tor!
        old_proto = session.config.sys.proxy.protocol
        session.config.sys.proxy.protocol = 'tor'
        session.config.sys.proxy.host = hostport[0]
        session.config.sys.proxy.port = hostport[1]
        session.config.sys.proxy.fallback = True

        # Configure connection broker, revert settings while we test
        ConnBroker.configure()
        session.config.sys.proxy.protocol = old_proto

        # Test it...
        need_tor = [ConnBroker.OUTGOING_HTTPS]
        try:
            with ConnBroker.context(need=need_tor) as context:
                motd = urlopen(MOTD_URL_TOR_ONLY_NO_MARS,
                               data=None, timeout=10).read()
                assert(motd.strip().endswith('}'))
            session.config.sys.proxy.protocol = 'tor'
            message = _('Successfully configured and enabled Tor!')
        except (IOError, AssertionError):
            ConnBroker.configure()
            message = _('Failed to configure Tor on %s:%s. Is the network down?'
                        ) % hostport
        return message
Beispiel #20
0
    def edit_messages(self, session, emails):
        if not self.interactive:
            return False

        for e in emails:
            if not e.is_editable():
                from mailpile.mailutils import NotEditableError
                raise NotEditableError(_('Message %s is not editable')
                                       % e.msg_mid())

        sep = '-' * 79 + '\n'
        edit_this = ('\n'+sep).join([e.get_editing_string() for e in emails])
        self.block()

        tf = tempfile.NamedTemporaryFile()
        tf.write(edit_this.encode('utf-8'))
        tf.flush()
        os.system('%s %s' % (os.getenv('VISUAL', default='vi'), tf.name))
        tf.seek(0, 0)
        edited = tf.read().decode('utf-8')
        tf.close()

        self.unblock()
        if edited == edit_this:
            return False

        updates = [t.strip() for t in edited.split(sep)]
        if len(updates) != len(emails):
            raise ValueError(_('Number of edit messages does not match!'))
        for i in range(0, len(updates)):
            emails[i].update_from_string(session, updates[i])
        return True
Beispiel #21
0
 def render_web(self, cfg, tpl_names, data):
     """Render data as HTML"""
     alldata = default_dict(self.html_variables)
     alldata["config"] = cfg
     alldata.update(data)
     try:
         template = self._web_template(cfg, tpl_names)
         if template:
             return template.render(alldata)
         else:
             emsg = _("<h1>Template not found</h1>\n<p>%s</p><p>"
                      "<b>DATA:</b> %s</p>")
             tpl_esc_names = [escape_html(tn) for tn in tpl_names]
             return emsg % (' or '.join(tpl_esc_names),
                            escape_html('%s' % alldata))
     except (UndefinedError, ):
         emsg = _("<h1>Template error</h1>\n"
                  "<pre>%s</pre>\n<p>%s</p><p><b>DATA:</b> %s</p>")
         return emsg % (escape_html(traceback.format_exc()),
                        ' or '.join([escape_html(tn) for tn in tpl_names]),
                        escape_html('%.4096s' % alldata))
     except (TemplateNotFound, TemplatesNotFound), e:
         emsg = _("<h1>Template not found in %s</h1>\n"
                  "<b>%s</b><br/>"
                  "<div><hr><p><b>DATA:</b> %s</p></div>")
         return emsg % tuple([escape_html(unicode(v))
                              for v in (e.name, e.message,
                                        '%.4096s' % alldata)])
Beispiel #22
0
    def setup_command(self, session):
        results = {}
        args = list(self.args)
        self.deadline = time.time() + float(self.data.get('timeout', [60])[0])
        self.tracking_id = self.data.get('track-id', [None])[0]
        self.password = self.data.get('password', [None])[0]
        if not self.password and len(args) > 1:
            self.password = args.pop(-1)

        emails = args + self.data.get('email', [])
        if self.password and len(emails) != 1:
            return self._error(_('Can only test settings for one account '
                                 'at a time'))

        for email in emails:
            settings = self._testing_data(self._get_email_settings,
                                          self.TEST_DATA, email)
            if settings:
                results[email] = settings
                if self.password and self.deadline > time.time():
                    errors = self._probe_account_settings(email, results)
                    if errors:
                        for k in ('routes', 'sources'):
                            if (settings.get(k) and
                                    not settings[k][0].get('username')):
                                results['login_failed'] = True

            if time.time() >= self.deadline:
                break
        if results:
            return self._success(
                _('Found settings for %d addresses') % len(results),
                result=results)
        else:
            return self._error(_('No settings found'))
Beispiel #23
0
    def command(self):
        if self.session.config.sys.lockdown:
            return self._error(_('In lockdown, doing nothing.'))

        key_files = self.data.get("key_file", []) + [a for a in self.args
                                                     if not '://' in a]
        key_urls = self.data.get("key_url", []) + [a for a in self.args
                                                   if '://' in a]
        key_data = []
        key_data.extend(self.data.get("key_data", []))
        for key_file in key_files:
            with open(key_file) as file:
                key_data.append(file.read())
        for key_url in key_urls:
            with ConnBroker.context(need=[ConnBroker.OUTGOING_HTTP]):
                uo = urllib2.urlopen(key_url)
            key_data.append(uo.read())

        rv = self._gnupg().import_keys('\n'.join(key_data))

        # Previous crypto evaluations may now be out of date, so we
        # clear the cache so users can see results right away.
        ClearParseCache(pgpmime=True)

        # Update the VCards!
        PGPKeysImportAsVCards(self.session,
                              arg=([i['fingerprint'] for i in rv['updated']] +
                                   [i['fingerprint'] for i in rv['imported']])
                              ).run()

        return self._success(_("Imported %d keys") % len(key_data), rv)
Beispiel #24
0
 def command(self, *args, **kwargs):
     if self.CONFIG_REQUIRED:
         if not self.session.config.loaded_config:
             return self._error(_('Please log in'))
         if mailpile.util.QUITTING:
             return self._error(_('Shutting down'))
     return self.command(*args, **kwargs)
Beispiel #25
0
def GenerateBootstrap(state):
    """
    Generate the gui-o-matic bootstrap sequence.

    Once this sequence completes, either we have failed and will die,
    or Mailpile (specifically `mailpile.plugins.gui`) will take over and
    start sending gui-o-matic commands to update the UI.
    """
    bootstrap = ["OK LISTEN"]

    if state.is_running:
        # If Mailpile is running already, connect and ask it to talk to us.
        bootstrap += [
            "show_main_window {}",
            "notify_user %s" % json.dumps({
                'message': _("Connecting to Mailpile")}),
            "set_next_error_message %s" % json.dumps({
                'message': _("Failed to connect to Mailpile!")}),
            "OK LISTEN HTTP: " + (
                '%sgui/%s/watch/%%PORT%%/' % (state.base_url, state.secret))]
    else:
        # If Mailpile is not running already, launch it in a screen session.
        bootstrap += [
            "show_splash_screen %s" % json.dumps(
                SPLASH_SCREEN(state, _("Launching Mailpile"))),
            "set_next_error_message %s" % json.dumps({
                'message': _("Failed to launch Mailpile!")}),
            "OK LISTEN TCP: " + (
                # FIXME: This should launch a screen session using the
                #        same concepts as multipile's mailpile-admin.
                'screen -S mailpile -d -m mailpile'
                ' --set="prefs.open_in_browser = false" '
                ' --gui=%PORT% --interact')]

    return '\n'.join(bootstrap)
Beispiel #26
0
        def maybe_delete_from_server(loc, src):
            # Delete from source, if that's our policy.
            if policy != 'move':
                return

            downloaded = list(set(src.keys()) & set(loc.source_map.keys()))
            downloaded.sort(key=self._msg_key_order)

            should = _('Should delete %d messages') % len(downloaded)
            if 'sources' in config.sys.debug and downloaded:
                session.ui.debug(should)

            if config.prefs.allow_deletion:
                try:
                    for i, key in enumerate(downloaded):
                        progress['deleting'] = '%d/%d' % (i+1, len(downloaded))
                        src.remove(key)
                    src.flush()
                except:
                    # Just ignore errors for now, we'll try again later.
                    if 'sources' in config.sys.debug:
                        session.ui.debug(traceback.format_exc())
            else:
                progress['deleting'] = '. '.join([
                    _('Deletion is disabled'), should])
Beispiel #27
0
def _update_scores(key_id, key_info, known_keys_list):
    """Update scores and score explanations"""
    key_info["score"] = sum([score for source, (score, reason)
                             in key_info.get('scores', {}).iteritems()
                             if source != 'Known keys'])

    # This is done here, not on the keychain lookup handler, in case
    # for some reason (e.g. UID changes on source keys) remote sources
    # suggest matches which our local search doesn't catch.
    if key_id in known_keys_list:
        score, reason = _score_validity(known_keys_list[key_id]["validity"],
                                        local=True)
        if score == 0:
            score += 9
            reason = _('Key is on keychain')

        key_info["on_keychain"] = True
        key_info['score'] += score
        key_info['scores']['Known keys'] = [score, reason]

    if "keysize" in key_info:
        bits = int(key_info["keysize"])
        score = bits // 1024
        key_info['score'] += score
        key_info['scores']['Key size'] = [score, _('Key is %d bits') % bits]

    sc, reason = max([(abs(score), reason)
                     for score, reason in key_info['scores'].values()])
    key_info['score_reason'] = '%s' % reason

    log_score = math.log(3 * abs(key_info['score']), 3)
    key_info['score_stars'] = (max(1, min(int(round(log_score)), 5))
                               * (-1 if (key_info['score'] < 0) else 1))
Beispiel #28
0
 def command(self):
     session, config = self.session, self.session.config
     handle, lines = self.args[0], self.args[1:]
     vcard = config.vcards.get_vcard(handle)
     if not vcard:
         return self._error('%s not found: %s' % (self.VCARD, handle))
     config.vcards.deindex_vcard(vcard)
     try:
         for l in lines:
             if '=' in l[:5]:
                 ln, l = l.split('=', 1)
                 vcard.set_line(int(ln.strip()), VCardLine(l.strip()))
             else:
                 vcard.add(VCardLine(l))
         vcard.save()
         return self._success(_("Added %d lines") % len(lines),
             result=self._vcard_list([vcard], simplify=True, info={
                 'updated': handle,
                 'added': len(lines)
             }))
     except KeyboardInterrupt:
         raise
     except:
         config.vcards.index_vcard(vcard)
         self._ignore_exception()
         return self._error(_('Error adding lines to %s') % handle)
     finally:
         config.vcards.index_vcard(vcard)
Beispiel #29
0
 def _load_state(self):
     with self._lock:
         config, my_config = self.session.config, self.my_config
         events = list(config.event_log.incomplete(source=self,
                                                   data_id=my_config._key))
         if events:
             self.event = events[0]
             if my_config.enabled:
                 self.event.message = _('Starting up')
             else:
                 self.event.message = _('Disabled')
         else:
             self.event = config.event_log.log(
                 source=self,
                 flags=Event.RUNNING,
                 message=_('Starting up'),
                 data={'id': my_config._key})
         self.event.data['name'] = my_config.name or _('Mail Source')
         if 'counters' not in self.event.data:
             self.event.data['counters'] = {}
         for c in ('copied_messages',
                   'indexed_messages',
                   'unknown_policies'):
             if c not in self.event.data['counters']:
                 self.event.data['counters'][c] = 0
Beispiel #30
0
    def sync_mail(self):
        """Iterates through all the mailboxes and scans if necessary."""
        def unsorted(l):
            l.sort(key=lambda k: random.randint(0, 500))
            return l
        config = self.session.config
        self._last_rescan_count = rescanned = errors = 0
        self._interrupt = None
        batch = self.RESCAN_BATCH_SIZE
        errors = rescanned = 0
        if self.session.config.sys.debug:
            batch = 5

        ostate = self._state
        for mbx_cfg in unsorted(self.my_config.mailbox.values()):
            try:
                state = {}
                # Generally speaking, we only rescan if a mailbox looks like
                # it has changed. However, 1/20th of the time we take a look
                # anyway just in case looks are deceiving.
                if batch > 0 and (self._has_mailbox_changed(mbx_cfg, state) or
                                  random.randint(0, 20) == 10):

                    self._state = 'Waiting...'
                    with GLOBAL_RESCAN_LOCK:
                        if self._check_interrupt(clear=False):
                            break
                        count = self.rescan_mailbox(mbx_cfg._key,
                                                    stop_after=batch)

                    if count >= 0:
                        batch -= count
                        if (count and batch > 0 and
                                not self._interrupt and
                                not mailpile.util.QUITTING):
                            self._mark_mailbox_rescanned(mbx_cfg, state)
                            rescanned += 1
                    else:
                        errors += 1
            except (NoSuchMailboxError, IOError, OSError):
                errors += 1
            except:
                self._log_status(_('Internal error'))
                raise

        self._state = 'Waiting...'
        with GLOBAL_RESCAN_LOCK:
            if not self._check_interrupt():
                self.discover_mailboxes()

        if errors:
            self._log_status(_('Rescanned %d mailboxes, failed to rescan %d'
                               ) % (rescanned, errors))
        else:
            self._log_status(_('Rescanned %d mailboxes') % rescanned)

        self._last_rescan_count = rescanned
        self._state = ostate
        return rescanned
Beispiel #31
0
def _lockdown_strict(config):
    if DISABLE_LOCKDOWN: return False
    if _lockdown(config) > 1:
        return _('In lockdown, doing nothing.')
    return in_disk_lockdown(config)
Beispiel #32
0
    def setup_command(self, session):

        # FIXME!

        return self._success(_('Configuring a key'), self._result())
Beispiel #33
0
    def setup_command(self, session):
        changed = authed = False
        results = {
            'secret_keys': self.list_secret_keys(),
        }
        error_info = None

        if self.data.get('_method') == 'POST' or self._testing():
            for key in self.HTTP_POST_VARS.keys():
                if key in (['choose_key', 'passphrase', 'passphrase_confirm'] +
                           TestableWebbable.HTTP_POST_VARS.keys()):
                    continue
                try:
                    val = self.data.get(key, [''])[0]
                    if val:
                        session.config.prefs[key] = self.TRUTHY[val.lower()]
                        changed = True
                except (ValueError, KeyError):
                    error_info = (_('Invalid preference'), {
                        'invalid_setting': True,
                        'variable': key
                    })
                    break

            choose_key = self.data.get('choose_key', [''])[0]
            if choose_key and not error_info:
                if (choose_key not in results['secret_keys']
                        and choose_key != '!CREATE'):
                    error_info = (_('Invalid key'), {
                        'invalid_key': True,
                        'chosen_key': choose_key
                    })

            try:
                # FIXME: Key changing is fubar
                if not error_info:
                    chosen_key = (
                        (not error_info)
                        and choose_key) or session.config.prefs.gpg_recipient
                    passphrase = self.data.get('passphrase', [''])[0]
                    passphrase2 = self.data.get('passphrase_confirm', [''])[0]
                    assert (passphrase == passphrase2)
                    if chosen_key == '!CREATE':
                        assert (passphrase != '')
                        sps = SecurePassphraseStorage(passphrase)
                    elif chosen_key:
                        sps = mailpile.auth.VerifyAndStorePassphrase(
                            session.config,
                            passphrase=passphrase,
                            key=chosen_key)
                    else:
                        sps = mailpile.auth.VerifyAndStorePassphrase(
                            session.config, passphrase=passphrase)
                    if not chosen_key:
                        choose_key = '!CREATE'
                    results['updated_passphrase'] = True
                    session.config.gnupg_passphrase.data = sps.data
                    mailpile.auth.SetLoggedIn(self)
            except AssertionError:
                error_info = (_('Invalid passphrase'), {
                    'invalid_passphrase': True,
                    'chosen_key': session.config.prefs.gpg_recipient
                })

            with BLOCK_HTTPD_LOCK, Idle_HTTPD():
                if choose_key and not error_info:
                    session.config.prefs.gpg_recipient = choose_key
                    self.make_master_key()
                    changed = True

                with Setup.KEY_WORKER_LOCK:
                    if ((not error_info) and
                        (session.config.prefs.gpg_recipient == '!CREATE')
                            and (Setup.KEY_CREATING_THREAD is None
                                 or Setup.KEY_CREATING_THREAD.failed)):
                        gk = GnuPGKeyGenerator(
                            sps=session.config.gnupg_passphrase,
                            on_complete=('notify',
                                         lambda: self.gpg_key_ready(gk)))
                        Setup.KEY_CREATING_THREAD = gk
                        Setup.KEY_CREATING_THREAD.start()

        results.update({
            'creating_key': (Setup.KEY_CREATING_THREAD is not None
                             and Setup.KEY_CREATING_THREAD.running),
            'creating_failed': (Setup.KEY_CREATING_THREAD is not None
                                and Setup.KEY_CREATING_THREAD.failed),
            'chosen_key':
            session.config.prefs.gpg_recipient,
            'prefs': {
                'index_encrypted': session.config.prefs.index_encrypted,
                'obfuscate_index': session.config.prefs.obfuscate_index,
                'encrypt_mail': session.config.prefs.encrypt_mail,
                'encrypt_index': session.config.prefs.encrypt_index,
                'encrypt_vcards': session.config.prefs.encrypt_vcards,
                'encrypt_events': session.config.prefs.encrypt_events,
                'encrypt_misc': session.config.prefs.encrypt_misc
            }
        })

        if changed:
            self._background_save(config=True)

        if error_info:
            return self._error(error_info[0],
                               info=error_info[1],
                               result=results)
        elif changed:
            return self._success(_('Updated crypto preferences'), results)
        else:
            return self._success(_('Configure crypto preferences'), results)
Beispiel #34
0
        elif hours_ago < 2:
            return _('%d hour') % hours_ago
        else:
            return _('%d hours') % hours_ago
    elif days_ago < 2:
        return _(ts.strftime('%A'))  #return _('%d day') % days_ago
    elif days_ago < 7:
        return _(ts.strftime('%A'))  #return _('%d days') % days_ago
    elif days_ago < 366:
        return _(ts.strftime("%b")) + ts.strftime(" %d")
    else:
        return _(ts.strftime("%b")) + ts.strftime(" %d %Y")


_translate_these = [
    _('Monday'),
    _('Mon'),
    _('Tuesday'),
    _('Tue'),
    _('Wednesday'),
    _('Wed'),
    _('Thursday'),
    _('Thu'),
    _('Friday'),
    _('Fri'),
    _('Saturday'),
    _('Sat'),
    _('Sunday'),
    _('Sun'),
    _('January'),
    _('Jan'),
Beispiel #35
0
def evaluate_sender_trust(config, email, tree):
    """
    This uses historic data from the search engine to refine and expand
    upon the states we get back from GnuPG and attempt to detect forgeries.

    The new potential signature states are:

      unsigned  We expected a signature from this sender but found none
      changed   The signature was made with a key we've rarely seen before
      signed    The signature was made with a key we've often seen before

    The first state depends on the user's ratio of signed to unsigned
    messages, the second two depend on how frequently we've seen a given
    key used for signatures vs. the total number of signatures.

    These states will supercede the states we get from GnuPG like so:

      * `none` becomes `unsigned`
      * `unknown` or `unverified` may become `changed`
      * `unverified` may become `signed`

    The constants used in this algorithm can be found and tweaked in the
    `prefs.key_trust` section of the configuration file.
    """
    sender = email.get_sender()
    if not sender:
        return

    tree['trust'] = {}
    trust = tree["trust"]

    # If this mail didn't come from outside, skip all this.
    # We don't vet ourselves for forgeries.
    # FIXME: THIS IS INSECURE. We need to fix this mechanism globally.
    message = email.get_msg()
    if 'x-mp-internal-sender' in message:
        trust["status"] = _("We trust ourselves")
        return tree

    # Calculate the default window we search for information. Don't include
    # the same day as the message was received, to not be fooled by other
    # junk that arrived the same day.
    days = config.prefs.key_trust.window_days
    msgts = long(email.get_msg_info(config.index.MSG_DATE), 36)
    end = msgts - (24 * 3600)
    begin = end - (days * 24 * 3600)
    scope = ['dates:%d..%d' % (begin, end), 'from:%s' % sender]

    messages_per_key = {}
    trust['counts'] = messages_per_key

    def count(name, terms):
        if name not in messages_per_key:
            # Note: using .as_set() will exclude spam and trash, which
            #       is almsot certainly a good thing.
            msgs = config.index.search(config.background, scope + terms)
            messages_per_key[name] = len(msgs.as_set())
        return messages_per_key[name]

    total = lambda: count('total', [])
    if total() < min(5, config.prefs.key_trust.threshold):
        # If we have too few messages within our desired window, try
        # expanding the window...
        scope[1] = 'dates:1970..%d' % end
        del messages_per_key['total']

        # Still too few? Abort.
        if total() < min(5, config.prefs.key_trust.threshold):
            trust["trust_unknown"] = True
            trust["warning"] = _("This sender's reputation is unknown")
            return tree

    signed = lambda: count('signed', ['has:signature'])
    swr = config.prefs.key_trust.sig_warn_pct / 100.0
    ktr = config.prefs.key_trust.key_trust_pct / 100.0
    knr = config.prefs.key_trust.key_new_pct / 100.0

    def update_siginfo(si, trust):
        stat = si["status"]
        keyid = si.get('keyinfo', '')[-16:].lower()

        # Unsigned message: if the ratio of total signed messages is
        # above config.prefs.sig_warn_pct percent, we EXPECT signatures
        # and warn the user if they're not present.
        if (stat == 'none') and (signed() > swr * total()):
            si["status"] = 'unsigned'
            trust["missing_signature"] = True

        # Compare email timestamp with the signature timestamp.
        # If they differ by a great deal, treat the signature as
        # invalid. This makes it much harder to copy old signed
        # content (undetected) into new messages.
        elif abs(msgts - si.get("timestamp", msgts)) > 7 * 24 * 3600:
            si["status"] = 'invalid'
            trust["invalid_signature"] = True

        # Signed by unverified key: Signal that we trust this key if
        # this is the key we've seen most of the time for this user.
        # This is TOFU-ish.
        elif (keyid and ('unverified' in stat)
              and (count(keyid, ['sig:%s' % keyid]) > ktr * signed())):
            si["status"] = stat.replace('unverified', 'signed')
            trust["signed"] = True

        # Signed by a key we have seen very rarely for this user. Gently
        # warn the user that something unsual is going on.
        elif (keyid and ('unverified' in stat or 'unknown' in stat)
              and (count(keyid, ['sig:%s' % keyid]) < knr * signed())):
            changed = "mixed-changed" if ("mixed" in stat) else "changed"
            si["status"] = changed
            trust["key_changed"] = True

        else:
            trust["%s_signature" % si["status"].replace("mixed-", "")] = True

    if signed() >= config.prefs.key_trust.threshold:
        if 'crypto' in tree:
            update_siginfo(tree['crypto']['signature'], tree["trust"])

        for skey in ('text_parts', 'html_parts', 'attachments'):
            for i, part in enumerate(tree[skey]):
                if 'crypto' in part:
                    update_siginfo(part['crypto']['signature'], {})

    if 'received' in message:
        headerprints = email.get_headerprints()
        term = 'hps:%s' % headerprints['sender']
        hps = count(term, [term])
        if hps < 2:
            trust["mua_or_mta_changed"] = True

    # Translate accumulated state into a "problem" if applicable
    problem = "problem" if (total() > 20) else "warning"
    if trust.get("invalid_signature") or trust.get("revoked_signature"):
        trust[problem] = _("The digital signature is invalid")
    elif trust.get("missing_signature"):
        trust[problem] = _("This person usually signs their mail")
    elif trust.get("key_changed"):
        trust[problem] = _("This was signed by an unexpected key")
    elif trust.get("expired_signature"):
        trust[problem] = _("This was signed by an expired key")
    elif trust.get("verified_signature") or trust.get("signed"):
        trust["status"] = _("Good signature, we are happy")
    elif trust.get("mua_or_mta_changed"):
        trust["warning"] = _("This came from an unexpected source")
    else:
        trust["status"] = _("No problems detected.")

    return tree
Beispiel #36
0
    def basic_app_config(self,
                         session,
                         save_and_update_workers=True,
                         want_daemons=True):
        # Create local mailboxes
        session.config.open_local_mailbox(session)

        # Create standard tags and filters
        created = []
        for t in self.TAGS:
            if not session.config.get_tag_id(t):
                AddTag(session, arg=[t]).run(save=False)
                created.append(t)
            session.config.get_tag(t).update(self.TAGS[t])
        for stype, statuses in (('sig', SignatureInfo.STATUSES),
                                ('enc', EncryptionInfo.STATUSES)):
            for status in statuses:
                tagname = 'mp_%s-%s' % (stype, status)
                if not session.config.get_tag_id(tagname):
                    AddTag(session, arg=[tagname]).run(save=False)
                    created.append(tagname)
                session.config.get_tag(tagname).update({
                    'type': 'attribute',
                    'display': 'invisible',
                    'label': False,
                })

        if 'New' in created:
            session.ui.notify(_('Created default tags'))

        # Import all the basic plugins
        reload_config = False
        for plugin in PLUGINS:
            if plugin not in session.config.sys.plugins:
                session.config.sys.plugins.append(plugin)
                reload_config = True
        if reload_config:
            with session.config._lock:
                session.config.save()
                session.config.load(session)

        try:
            # If spambayes is not installed, this will fail
            import mailpile.plugins.autotag_sb
            if 'autotag_sb' not in session.config.sys.plugins:
                session.config.sys.plugins.append('autotag_sb')
                session.ui.notify(_('Enabling spambayes autotagger'))
        except ImportError:
            session.ui.warning(
                _('Please install spambayes '
                  'for super awesome spam filtering'))

        vcard_importers = session.config.prefs.vcard.importers
        if not vcard_importers.gravatar:
            vcard_importers.gravatar.append({'active': True})
            session.ui.notify(_('Enabling gravatar image importer'))

        gpg_home = os.path.expanduser('~/.gnupg')
        if os.path.exists(gpg_home) and not vcard_importers.gpg:
            vcard_importers.gpg.append({'active': True, 'gpg_home': gpg_home})
            session.ui.notify(_('Importing contacts from GPG keyring'))

        if ('autotag_sb' in session.config.sys.plugins
                and len(session.config.prefs.autotag) == 0):
            session.config.prefs.autotag.append({
                'match_tag': 'spam',
                'unsure_tag': 'maybespam',
                'tagger': 'spambayes',
                'trainer': 'spambayes'
            })
            session.config.prefs.autotag[0].exclude_tags[0] = 'ham'

        if save_and_update_workers:
            session.config.save()
            session.config.prepare_workers(session, daemons=want_daemons)
class MailpileCommand(Extension):
    """Run Mailpile Commands, """
    tags = set(['mpcmd'])

    def __init__(self, environment):
        Extension.__init__(self, environment)
        e = self.env = environment
        s = self
        e.globals['mailpile'] = s._command
        e.globals['mailpile_render'] = s._command_render
        e.globals['U'] = s._url_path_fix
        e.globals['make_rid'] = randomish_uid
        e.globals['is_dev_version'] = s._is_dev_version
        e.globals['is_configured'] = s._is_configured
        e.globals['version_identifier'] = s._version_identifier
        e.filters['random'] = s._random
        e.globals['random'] = s._random
        e.filters['truthy'] = s._truthy
        e.globals['truthy'] = s._truthy
        e.filters['with_context'] = s._with_context
        e.globals['with_context'] = s._with_context
        e.filters['url_path_fix'] = s._url_path_fix
        e.globals['use_data_view'] = s._use_data_view
        e.globals['regex_replace'] = s._regex_replace
        e.filters['regex_replace'] = s._regex_replace
        e.globals['friendly_bytes'] = s._friendly_bytes
        e.filters['friendly_bytes'] = s._friendly_bytes
        e.globals['friendly_number'] = s._friendly_number
        e.filters['friendly_number'] = s._friendly_number
        e.globals['show_avatar'] = s._show_avatar
        e.filters['show_avatar'] = s._show_avatar
        e.globals['navigation_on'] = s._navigation_on
        e.filters['navigation_on'] = s._navigation_on
        e.globals['has_label_tags'] = s._has_label_tags
        e.filters['has_label_tags'] = s._has_label_tags
        e.globals['show_message_signature'] = s._show_message_signature
        e.filters['show_message_signature'] = s._show_message_signature
        e.globals['show_message_encryption'] = s._show_message_encryption
        e.filters['show_message_encryption'] = s._show_message_encryption
        e.globals['show_text_part_signature'] = s._show_text_part_signature
        e.filters['show_text_part_signature'] = s._show_text_part_signature
        e.globals['show_text_part_encryption'] = s._show_text_part_encryption
        e.filters['show_text_part_encryption'] = s._show_text_part_encryption
        e.globals['show_crypto_policy'] = s._show_crypto_policy
        e.filters['show_crypto_policy'] = s._show_crypto_policy
        e.globals['contact_url'] = s._contact_url
        e.filters['contact_url'] = s._contact_url
        e.globals['contact_name'] = s._contact_name
        e.filters['contact_name'] = s._contact_name
        e.globals['thread_upside_down'] = s._thread_upside_down
        e.filters['thread_upside_down'] = s._thread_upside_down
        e.globals['fix_urls'] = s._fix_urls
        e.filters['fix_urls'] = s._fix_urls

        # See utils.py for these functions:
        e.globals['elapsed_datetime'] = elapsed_datetime
        e.filters['elapsed_datetime'] = elapsed_datetime
        e.globals['friendly_datetime'] = friendly_datetime
        e.filters['friendly_datetime'] = friendly_datetime
        e.globals['friendly_time'] = friendly_time
        e.filters['friendly_time'] = friendly_time

        # These are helpers for injecting plugin elements
        e.globals['get_ui_elements'] = s._get_ui_elements
        e.globals['ui_elements_setup'] = s._ui_elements_setup
        e.globals['add_state_query_string'] = s._add_state_query_string
        e.filters['add_state_query_string'] = s._add_state_query_string

        # This is a worse versin of urlencode, but without it we require
        # Jinja 2.7, which isn't apt-get installable.
        e.globals['urlencode'] = s._urlencode
        e.filters['urlencode'] = s._urlencode
        # Same thing for selectattr
        e.globals['selectattr'] = s._selectattr
        e.filters['selectattr'] = s._selectattr

        # Make a function-version of the safe command
        e.globals['safe'] = s._safe
        e.filters['json'] = s._json
        e.filters['escapejs'] = s._escapejs

        # Strip trailing blank lines from email
        e.globals['nice_text'] = s._nice_text
        e.filters['nice_text'] = s._nice_text

        # Transforms \n into HTML <br />
        e.globals['to_br'] = s._to_br
        e.filters['to_br'] = s._to_br

        # Strip Re: Fwd: from subject lines
        e.globals['nice_subject'] = s._nice_subject
        e.filters['nice_subject'] = s._nice_subject
        # And [list] headings as well
        e.globals['bare_subject'] = s._bare_subject
        e.filters['bare_subject'] = s._bare_subject

        # Make unruly names a lil bit nicer
        e.globals['nice_name'] = s._nice_name
        e.filters['nice_name'] = s._nice_name

        # Makes a UI usable classification of attachment from mimetype
        e.globals['attachment_type'] = s._attachment_type
        e.filters['attachment_type'] = s._attachment_type

        # Loads theme settings JSON manifest
        e.globals['theme_settings'] = s._theme_settings
        e.filters['theme_settings'] = s._theme_settings

        # Separates Fingerprint in 4 char groups
        e.globals['nice_fingerprint'] = s._nice_fingerprint
        e.filters['nice_fingerprint'] = s._nice_fingerprint

        # Converts Filter +/- tags into arrays
        e.globals['make_filter_groups'] = s._make_filter_groups
        e.filters['make_filter_groups'] = s._make_filter_groups

        # Make Nice Summary of Recipients
        e.globals['recipient_summary'] = s._recipient_summary
        e.filters['recipient_summary'] = s._recipient_summary

        # Nagifications
        e.globals['show_nagification'] = s._show_nagification
        e.filters['show_nagification'] = s._show_nagification

    def _debug(self, msg):
        if 'jinja' in self.env.session.config.sys.debug:
            sys.stderr.write('jinja: ')
            sys.stderr.write(msg)
            sys.stderr.write('\n')
            sys.stderr.flush()

    def _command(self, command, *args, **kwargs):
        rv = Action(self.env.session, command, args, data=kwargs).as_dict()
        self._debug('mailpile(%s, %s, %s) -> %s' % (command, args, kwargs, rv))
        return rv

    def _command_render(self, how, command, *args, **kwargs):
        self._debug('mailpile_render(%s, %s, ...)' % (how, command))
        old_ui, config = self.env.session.ui, self.env.session.config
        try:
            ui = self.env.session.ui = HttpUserInteraction(None,
                                                           config,
                                                           log_parent=old_ui,
                                                           log_prefix='jinja/')
            ui.html_variables = copy.deepcopy(old_ui.html_variables)
            ui.render_mode = how
            ui.display_result(
                Action(self.env.session, command, args, data=kwargs))
            rv = ui.render_response(config)
            return (rv[0], rv[1].strip())
        finally:
            self.env.session.ui = old_ui

    def _use_data_view(self, view_name, result):
        self._debug('use_data_view(%s, ...)' % (view_name))
        dv = UrlMap(self.env.session).map(None, 'GET', view_name, {}, {})[-1]
        return dv.view(result)

    def _get_ui_elements(self, ui_type, state, context=None):
        self._debug('get_ui_element(%s, %s, ...)' % (ui_type, state))
        ctx = context or state.get('context_url', '')
        return copy.deepcopy(PluginManager().get_ui_elements(ui_type, ctx))

    @classmethod
    def _add_state_query_string(cls, url, state, elem=None):
        if not url:
            url = state.get('command_url', '')
        if '#' in url:
            url, frag = url.split('#', 1)
            frag = '#' + frag
        else:
            frag = ''
        if url:
            args = []
            query_args = state.get('query_args', {})
            for key in sorted(query_args.keys()):
                if key.startswith('_'):
                    continue
                values = query_args[key]
                if elem:
                    for rk, rv in elem.get('url_args_remove', []):
                        if rk == key:
                            values = [v for v in values if rv and (v != rv)]
                if elem:
                    for ak, av in elem.get('url_args_add', []):
                        if ak == key and av not in values:
                            values.append(av)
                args.extend([(key, unicode(v).encode("utf-8"))
                             for v in values])
            return url + '?' + urllib.urlencode(args) + frag
        else:
            return url + frag

    def _ui_elements_setup(self, classfmt, elements):
        self._debug('ui_elements_setup(%s, %s)' % (classfmt, elements))
        setups = []
        for elem in elements:
            if elem.get('javascript_setup'):
                setups.append('$("%s").each(function(){%s(this);});' %
                              (classfmt % elem, elem['javascript_setup']))
            if elem.get('javascript_events'):
                for event, call in elem.get('javascript_events').iteritems():
                    setups.append('$("%s").bind("%s", %s);' %
                                  (classfmt % elem, event, call))
        return Markup("function(){%s}" % ''.join(setups))

    def _regex_replace(self, s, find, replace):
        """A non-optimal implementation of a regex filter"""
        return re.sub(find, replace, s)

    def _friendly_number(self, number, decimals=0):
        # See mailpile/util.py:friendly_number if this needs fixing
        return friendly_number(number, decimals=decimals, base=1000)

    def _friendly_bytes(self, number, decimals=0):
        # See mailpile/util.py:friendly_number if this needs fixing
        return friendly_number(number,
                               decimals=decimals,
                               base=1024,
                               suffix='B')

    def _show_avatar(self, contact):
        if "photo" in contact:
            photo = contact['photo']
        else:
            photo = ('%s/static/img/avatar-default.png' %
                     self.env.session.config.sys.http_path)
        return photo

    def _navigation_on(self, search_tag_ids, on_tid):
        if search_tag_ids:
            for tid in search_tag_ids:
                if tid == on_tid:
                    return "navigation-on"
                else:
                    return ""

    def _has_label_tags(self, tags, tag_tids):
        self._debug('has_label_tags(..., %s, ...)' % (tag_tids, ))
        count = 0
        for tid in tag_tids:
            if tags[tid]["label"] and not tags[tid]["searched"]:
                count += 1
        return count

    _DEFAULT_SIGNATURE = [
        "crypto-color-gray", "icon-signature-none",
        _("Unknown"),
        _("There is something unknown or wrong with this signature")
    ]
    _STATUS_SIGNATURE = {
        "none": [
            "crypto-color-gray", "icon-signature-none",
            _("Not Signed"),
            _("This data has no digital signature, which means it could "
              "have come from anyone, not necessarily the real sender")
        ],
        "error": [
            "crypto-color-red", "icon-signature-error",
            _("Error"),
            _("There was a weird error with this digital signature")
        ],
        "mixed-error": [
            "crypto-color-red", "icon-signature-error",
            _("Mixed Error"),
            _("Parts of this message have a signature with a weird error")
        ],
        "invalid": [
            "crypto-color-red", "icon-signature-invalid",
            _("Invalid"),
            _("The digital signature was invalid or bad")
        ],
        "mixed-invalid": [
            "crypto-color-red", "icon-signature-invalid",
            _("Mixed Invalid"),
            _("Parts of this message have a digital signature that is invalid"
              " or bad")
        ],
        "revoked": [
            "crypto-color-red", "icon-signature-revoked",
            _("Revoked"),
            _("Watch out, the digital signature was made with a key that has been "
              "revoked - this is not a good thing")
        ],
        "mixed-revoked": [
            "crypto-color-red", "icon-signature-revoked",
            _("Mixed Revoked"),
            _("Watch out, parts of this message were digitally signed with a key "
              "that has been revoked")
        ],
        "expired": [
            "crypto-color-orange", "icon-signature-expired",
            _("Expired"),
            _("The digital signature was made with an expired key")
        ],
        "mixed-expired": [
            "crypto-color-orange", "icon-signature-expired",
            _("Mixed Expired"),
            _("Parts of this message have a digital signature made with an "
              "expired key")
        ],
        "unknown": [
            "crypto-color-gray", "icon-signature-unknown",
            _("Unknown"),
            _("The digital signature was made with an unknown key, so we can not "
              "verify it")
        ],
        "mixed-unknown": [
            "crypto-color-gray", "icon-signature-unknown",
            _("Mixed Unknown"),
            _("Parts of this message have a signature made with an unknown "
              "key which we can not verify")
        ],
        "unverified": [
            "crypto-color-blue", "icon-signature-unverified",
            _("Unverified"),
            _("The signature was good but it came from a key that is not "
              "verified yet")
        ],
        "mixed-unverified": [
            "crypto-color-blue", "icon-signature-unverified",
            _("Mixed Unverified"),
            _("Parts of this message have an unverified signature")
        ],
        "verified": [
            "crypto-color-green", "icon-signature-verified",
            _("Verified"),
            _("The signature was good and came from a verified key, w00t!")
        ],
        "mixed-verified": [
            "crypto-color-blue", "icon-signature-verified",
            _("Mixed Verified"),
            _("Parts of the message have a verified signature, but other "
              "parts do not")
        ]
    }

    @classmethod
    def _show_text_part_signature(self, status):
        # Within a text part, mixed state is equivalent to no encryption, and
        # no signature - the signed/encrypted parts are explictly marked.
        try:
            if status and status.startswith('mixed-'):
                status = 'none'
        except UndefinedError:
            status = 'none'
        return self._show_message_signature(status)

    @classmethod
    def _show_message_signature(self, status):
        # This avoids crashes when attributes are missing.
        try:
            if status.startswith('hack the planet'):
                pass
        except UndefinedError:
            status = ''

        color, icon, text, message = self._STATUS_SIGNATURE.get(
            status, self._DEFAULT_SIGNATURE)

        return {'color': color, 'icon': icon, 'text': text, 'message': message}

    _DEFAULT_ENCRYPTION = [
        "crypto-color-gray", "icon-lock-open",
        _("Unknown"),
        _("There is some unknown thing wrong with this encryption")
    ]
    _STATUS_ENCRYPTION = {
        "none": [
            "crypto-color-gray", "icon-lock-open",
            _("Not Encrypted"),
            _("This content was not encrypted. It could have been intercepted "
              "and read by an unauthorized party")
        ],
        "decrypted": [
            "crypto-color-green", "icon-lock-closed",
            _("Encrypted"),
            _("This content was encrypted, great job being secure")
        ],
        "mixed-decrypted": [
            "crypto-color-blue", "icon-lock-closed",
            _("Mixed Encrypted"),
            _("Part of this message were encrypted, but other parts were not "
              "encrypted")
        ],
        "lockedkey": [
            "crypto-color-green", "icon-lock-closed",
            _("Locked Key"),
            _("You have the encryption key to decrypt this, "
              "but the key itself is locked.")
        ],
        "mixed-lockedkey": [
            "crypto-color-green", "icon-lock-closed",
            _("Mixed Locked Key"),
            _("Parts of the message could not be decrypted because your "
              "encryption key is locked.")
        ],
        "missingkey": [
            "crypto-color-red", "icon-lock-closed",
            _("Missing Key"),
            _("You don't have the encryption key to decrypt this, "
              "perhaps it was encrypted to an old key you don't have anymore?")
        ],
        "mixed-missingkey": [
            "crypto-color-red", "icon-lock-closed",
            _("Mixed Missing Key"),
            _("Parts of the message could not be decrypted because you "
              "are missing the private key. Perhaps it was encrypted to an "
              "old key you don't have anymore?")
        ],
        "error": [
            "crypto-color-red", "icon-lock-error",
            _("Error"),
            _("We failed to decrypt and are unsure why.")
        ],
        "mixed-error": [
            "crypto-color-red", "icon-lock-error",
            _("Mixed Error"),
            _("We failed to decrypt parts of this message and are unsure why")
        ]
    }

    @classmethod
    def _show_text_part_encryption(self, status):
        # Within a text part, mixed state is equivalent to no encryption, and
        # no signature - the signed/encrypted parts are explictly marked.
        try:
            if status and status.startswith('mixed-'):
                status = 'none'
        except UndefinedError:
            status = 'none'
        return self._show_message_encryption(status)

    @classmethod
    def _show_message_encryption(self, status):
        # This avoids crashes when attributes are missing.
        try:
            if status.startswith('hack the planet'):
                pass
        except UndefinedError:
            status = ''

        color, icon, text, message = self._STATUS_ENCRYPTION.get(
            status, self._DEFAULT_ENCRYPTION)

        return {'color': color, 'icon': icon, 'text': text, 'message': message}

    _DEFAULT_CRYPTO_POLICY = [
        _("Automatic"),
        _("Mailpile will intelligently try to guess and suggest the best "
          "security with this given contact")
    ]
    _CRYPTO_POLICY = {
        "default": [
            _("Automatic"),
            _("Mailpile will intelligently try to guess and suggest the best "
              "security with this given contact")
        ],
        "none": [
            _("Don't Sign or Encrypt"),
            _("Messages will not be encrypted nor signed by your encryption key"
              )
        ],
        "sign": [
            _("Only Sign"),
            _("Messages will only be signed by your encryption key")
        ],
        "encrypt": [
            _("Only Encrypt"),
            _("Messages will only be encrypted but not signed by your encryption key"
              )
        ],
        "sign-encrypt": [
            _("Always Encrypt & Sign"),
            _("Messages will be both encrypted and signed by your encryption key"
              )
        ]
    }

    @classmethod
    def _show_crypto_policy(self, policy):
        # This avoids crashes when attributes are missing.
        try:
            if policy.startswith('hack the planet'):
                pass
        except UndefinedError:
            policy = ''

        text, message = self._CRYPTO_POLICY.get(policy,
                                                self._DEFAULT_CRYPTO_POLICY)

        return {'text': text, 'message': message}

    def _contact_url(self, person):
        if not self._is_dev_version():
            return ('%s/search/?q=email:%s') % (
                self.env.session.config.sys.http_path, person['address'])

        if 'contact' in person['flags']:
            url = ("%s/contacts/view/%s/" %
                   (self.env.session.config.sys.http_path, person['address']))
        else:
            url = "%s/#add-contact" % self.env.session.config.sys.http_path
        return url

    def _contact_name(self, person):
        self._debug('contact_name(%s)' % (person, ))
        name = person['fn']
        if (not name or '@' in name) and person.get('email'):
            vcard = self.env.session.config.vcards.get_vcard(person['email'])
            if vcard:
                return vcard.fn
        return name

    @classmethod
    def _thread_upside_down(self, thread):
        return [(i, flip_unicode_boxes(a), c) for i, a, c in reversed(thread)]

    URL_RE_HTTP = re.compile('(<a [^>]*?)'  # 1: <a
                             '(href=["\'])'  # 2:    href="
                             '(https?:[^>]+)'  # 3:  URL!
                             '(["\'][^>]*>)'  # 4:          ">
                             '(.*?)'  # 5:  Description!
                             '(</a>)')  # 6: </a>

    # We deliberately leave the https:// prefix on, because it is both
    # rare and worth drawing attention to.
    URL_RE_HTTP_PROTO = re.compile('(?i)^https?://((w+\d*|[a-z]+\d+)\.)?')

    URL_RE_MAILTO = re.compile('(<a [^>]*?)'  # 1: <a
                               '(href=["\']mailto:)'  # 2:    href="mailto:
                               '([^"]+)'  # 3:  Email address!
                               '(["\'][^>]*>)'  # 4:          ">
                               '(.*?)'  # 5:  Description!
                               '(</a>)')  # 6: </a>

    URL_DANGER_ALERT = ('onclick=\'return confirm("' +
                        _("Mailpile security tip: \\n\\n"
                          "  Uh oh! This web site may be dangerous!\\n"
                          "  Are you sure you want to continue?\\n") + '");\'')

    def _fix_urls(self, text, truncate=45, danger=False):
        def http_fixer(m):
            url = m.group(3)
            odesc = desc = m.group(5)
            url_danger = danger

            if len(desc) > truncate:
                desc = desc[:truncate - 3] + '...'
                noproto = re.sub(self.URL_RE_HTTP_PROTO, '', desc)
                if ('/' not in noproto) and ('?' not in noproto):
                    # Phishers sometimes create subdomains that look like
                    # something legit: yourbank.evil.com.
                    # So, if the domain was getting truncated reveal the TLD
                    # even if that means overflowing our truncation request.
                    noproto = re.sub(self.URL_RE_HTTP_PROTO, '', odesc)
                    if '/' in noproto:
                        desc = '.'.join(
                            noproto.split('/')[0].rsplit('.', 3)[-2:]) + '/...'
                    else:
                        desc = '.'.join(
                            noproto.split('?')[0].rsplit('.', 3)[-2:]) + '/...'
                    url_danger = True

            return ''.join([
                m.group(1), url_danger and self.URL_DANGER_ALERT or '',
                ' target=_blank ',
                m.group(2), url,
                m.group(4), desc,
                m.group(6)
            ])

        def mailto_fixer(m):
            return ''.join([
                m.group(1), 'href="mailto:',
                m.group(3), '" class="compose-to-email">',
                m.group(5),
                m.group(6)
            ])

        return Markup(
            re.sub(self.URL_RE_HTTP, http_fixer,
                   re.sub(self.URL_RE_MAILTO, mailto_fixer, text)))

    def _random(self, sequence):
        return sequence[random.randint(0, len(sequence) - 1)]

    @classmethod
    def _truthy(cls, txt, default=False):
        return truthy(txt, default=default)

    @classmethod
    def _is_dev_version(cls):
        return ('dev' in APPVER or 'github' in APPVER or 'test' in APPVER)

    def _is_configured(self):
        return (self.env.session.config.prefs.web_content != "unknown")

    @classmethod
    def _version_identifier(cls):
        return VERSION_IDENTIFIER or APPVER

    def _with_context(self, sequence, context=1):
        return [[(sequence[j] if (0 <= j < len(sequence)) else None)
                 for j in range(i - context, i + context + 1)]
                for i in range(0, len(sequence))]

    def _url_path_fix(self, *urlparts):
        url = ''.join([unicode(p) for p in urlparts])
        if url[:1] in ('/', ):
            http_path = self.env.session.config.sys.http_path or ''
            if not url.startswith(http_path):
                url = http_path + url
        return self._safe(url)

    def _urlencode(self, s):
        if type(s) == 'Markup':
            s = s.unescape()
        return Markup(urllib.quote_plus(s.encode('utf-8')))

    def _selectattr(self, seq, attr, value=None):
        if value is None:
            return [s for s in seq if s.get(attr)]
        else:
            return [s for s in seq if s.get(attr) == value]

    def _safe(self, s):
        if type(s) == 'Markup':
            return s.unescape()
        else:
            return Markup(s).unescape()

    def _json(self, d):
        json = self.env.session.ui.render_json(d)
        # These are necessary so the browser doesn't get confused by things
        # when JSON is included directly into the HTML as a <script>.
        json = json.replace('<', '\\x3c')
        json = json.replace('&', '\\x26')
        return json

    _JS_ESCAPES = (
        ('\\', '\\x5c'),
        ('\'', '\\x27'),
        ('"', '\\x22'),
        ('>', '\\x3e'),
        ('<', '\\x3c'),
        ('&', '\\x26'),
        ('=', '\\x3d'),
        ('-', '\\x2d'),
        (';', '\\x3b'),
    )

    def _escapejs(self, value):
        """ Hex encodes some characters for use in JavaScript strings.

        Lightly inspired from https://github.com/django/django/blame/ebc773ada3e4f40cf5084268387b873d7fe22e8b/django/utils/html.py#L63
        """
        for bad, good in self._JS_ESCAPES:
            value = value.replace(bad, good)
        return self._safe(value)

    @classmethod
    def _nice_text(self, text):
        trimmed = ''
        previous = 'not'
        for line in text.splitlines():
            if line or previous == 'not':
                trimmed += line + '\n'
                if line:
                    previous = 'not'
                else:
                    previous = 'blank'
        return trimmed.strip()

    _TEXT_LINEBREAK_RE = re.compile(r'(?:\r\n|\r|\n)')

    @classmethod
    def _to_br(self, text):
        """ Replaces \n by <br />

        Inspired from http://jinja.pocoo.org/docs/dev/api/#custom-filters
        """
        result = '<br />'.join(
            p for p in self._TEXT_LINEBREAK_RE.split(escape(text)))
        return Markup(result)

    @classmethod
    def _nice_subject(self, metadata):
        if metadata['subject']:
            output = re.sub('(?i)^((re|fw|fwd|aw|wg):\s+)+', '',
                            metadata['subject'])
        else:
            output = '(' + _("No Subject") + ')'
        return output

    @classmethod
    def _bare_subject(self, metadata):
        if metadata['subject']:
            output = re.sub('(?i)^((re|fw|fwd|aw|wg):\s+|\[\S+\]\s+)+', '',
                            metadata['subject'])
        else:
            output = '(' + _("No Subject") + ')'
        return output

    @classmethod
    def _nice_name(self, name, truncate=100):
        if len(name) > truncate:
            name = name[:truncate - 3] + '...'
        return name

    @classmethod
    def _recipient_summary(self, editing_strings, addresses, truncate):
        summary_list = []
        recipients = (editing_strings['to_aids'] + editing_strings['cc_aids'] +
                      editing_strings['bcc_aids'])
        for aid in recipients:
            summary_list.append(addresses[aid].fn)
        summary = ', '.join(summary_list)
        if len(summary) > truncate:
            others = ''
            if len(recipients) > 1:
                others = _("and %d others") % (len(recipients) - 1)
            summary = summary[:truncate] + '... ' + others
        return summary

    @classmethod
    def _attachment_type(self, mime):
        if mime in [
                "application/octet-stream", "application/mac-binhex40",
                "application/x-shockwave-flash", "application/x-director",
                "application/x-x509-ca-cert", "application/x-director",
                "application/x-msdownload", "application/x-director"
        ]:
            attachment = "application"
        elif mime in [
                "application/x-compress", "application/x-compressed",
                "application/x-tar", "application/zip",
                "application/x-stuffit", "application/x-gzip",
                "application/x-gzip-compressed", "application/x-tar",
                "application/x-winzip", "application/x-zip",
                "application/x-zip-compressed"
        ]:
            attachment = "archive"
        elif mime in [
                "audio/midi", "audio/mid", "audio/mpeg", "audio/basic",
                "audio/x-aiff", "audio/x-pn-realaudio", "audio/x-pn-realaudio",
                "audio/mid", "audio/basic", "audio/x-wav", "audio/x-mpegurl",
                "audio/wave", "audio/wav"
        ]:
            attachment = "audio"
        elif mime in ["text/x-vcard"]:
            attachment = "contact"
        elif mime in [
                "image/bmp", "image/gif", "image/jpeg", "image/pjpeg",
                "image/svg+xml", "image/x-png", "image/png"
        ]:
            attachment = "image-visible"
        elif mime in [
                "image/cis-cod", "image/ief", "image/pipeg", "image/tiff",
                "image/x-cmx", "image/x-cmu-raster", "image/x-rgb",
                "image/x-icon", "image/x-xbitmap", "image/x-xpixmap",
                "image/x-xwindowdump", "image/x-portable-anymap",
                "image/x-portable-graymap", "image/x-portable-pixmap",
                "image/x-portable-bitmap", "application/x-photoshop",
                "application/postscript"
        ]:
            attachment = "image"
        elif mime in ["application/pgp-signature"]:
            attachment = "signature"
        elif mime in ["application/pgp-keys"]:
            attachment = "keys"
        elif mime in [
                "application/rtf", "application/vnd.ms-works",
                "application/msword", "application/pdf",
                "application/x-download", "message/rfc822", "text/scriptlet",
                "text/plain", "text/iuls", "text/plain", "text/richtext",
                "text/x-setext", "text/x-component", "text/webviewhtml",
                "text/h323"
        ]:
            attachment = "document"
        elif mime in [
                "application/x-javascript", "text/html", "text/css",
                "text/xml", "text/json"
        ]:
            attachment = "code"
        elif mime in [
                "application/excel", "application/msexcel",
                "application/vnd.ms-excel", "application/vnd.msexcel",
                "application/csv", "application/x-csv",
                "text/tab-separated-values", "text/x-comma-separated-values",
                "text/comma-separated-values", "text/csv", "text/x-csv"
        ]:
            attachment = "spreadsheet"
        elif mime in [
                "application/powerpoint", "application/vnd.ms-powerpoint"
        ]:
            attachment = "slideshow"
        elif mime in [
                "video/quicktime", "video/x-sgi-movie", "video/mpeg",
                "video/x-la-asf", "video/x-ms-asf", "video/x-msvideo"
        ]:
            attachment = "video"
        else:
            attachment = "unknown"
        return attachment

    def _theme_settings(self):
        self._debug('theme_settings()')
        path, handle, mime = self.env.session.config.open_file(
            'html_theme', 'theme.json')
        return json.load(handle)

    def _nice_fingerprint(self, fingerprint):
        if fingerprint:
            slices = [
                fingerprint[i:i + 4] for i in range(0, len(fingerprint), 4)
            ]
            output = ""
            for group in slices:
                output += group + " "
            return output
        else:
            return _("No Fingerprint")

    def _make_filter_groups(self, tags):
        split = shlex.split(tags)
        output = dict()
        add = []
        remove = []
        for item in split:
            out = item.strip('+-')
            if item[0] == "+":
                add.append(out)
            elif item[0] == "-":
                remove.append(out)
        output['add'] = add
        output['remove'] = remove
        return output

    def _show_nagification(self, nag):
        now = long((time.time() + 0.5) * 1000)
        if now > nag and nag != -1:
            return True
        return False
Beispiel #38
0
    def command(self):
        session, config, index = self.session, self.session.config, self._idx()
        event_log = config.event_log

        incomplete = (self.data.get('incomplete', ['no']
                                    )[0].lower() not in self._FALSE)
        waiting = int(self.data.get('wait', [0])[0])
        gather = float(self.data.get('gather', [self.GATHER_TIME])[0])

        limit = 0
        filters = {}
        for arg in self.args:
            if arg.lower() == 'incomplete':
                incomplete = True
            elif arg.lower() == 'wait':
                waiting = self.DEFAULT_WAIT_TIME
            elif '=' in arg:
                field, value = arg.split('=', 1)
                filters[unicode(field)] = unicode(value)
            else:
                try:
                    limit = int(arg)
                except ValueError:
                    raise UsageError('Bad argument: %s' % arg)

        # Handle args from the web
        def fset(arg, val):
            if val.startswith('!'):
                filters[arg+'!'] = val[1:]
            else:
                filters[arg] = val
        for arg in self.data:
            if arg in ('source', 'flags', 'flag', 'since', 'event_id'):
                fset(arg, self.data[arg][0])
            elif arg in ('data', 'private_data'):
                for data in self.data[arg]:
                    var, val = data.split(':', 1)
                    fset('%s_%s' % (arg, var), val)

        # Compile regular expression matches
        for arg in filters:
            if filters[arg][:1] == '~':
                filters[arg] = re.compile(filters[arg][1:])

        now = time.time()
        expire = now + waiting - gather
        if waiting:
            if 'since' not in filters:
                filters['since'] = now
            if float(filters['since']) < 0:
                filters['since'] = float(filters['since']) + now
            time.sleep(gather)

        events = []
        while not waiting or (expire + gather) > time.time():
            if incomplete:
                events = list(config.event_log.incomplete(**filters))
            else:
                events = list(config.event_log.events(**filters))
            if events or not waiting:
                break
            else:
                config.event_log.wait(expire - time.time())
                time.sleep(gather)

        if limit:
            if 'since' in filters:
                events = events[:limit]
            else:
                events = events[-limit:]

        return self._success(_('Found %d events') % len(events),
                             result={
            'count': len(events),
            'ts': max([0] + [e.ts for e in events]) or time.time(),
            'events': [e.as_dict() for e in events]
        })
Beispiel #39
0
def _lockdown_quit(config):
    if DISABLE_LOCKDOWN: return False
    # The user is always allowed to quit, except in demo mode.
    if _lockdown(config) < 0:
        return _('In lockdown, doing nothing.')
    return False
Beispiel #40
0
def _lockdown_minimal(config):
    if DISABLE_LOCKDOWN: return False
    if _lockdown(config) != 0:
        return _('In lockdown, doing nothing.')
    return False
Beispiel #41
0
 def _score(self, key):
     return (self.SCORE, _('Found encryption key in keyserver'))
Beispiel #42
0
def lookup_crypto_keys(session,
                       address,
                       event=None,
                       strict_email_match=False,
                       allowremote=True,
                       origins=None,
                       get=None):
    known_keys_list = GnuPG(session and session.config or None).list_keys()
    found_keys = {}
    ordered_keys = []

    if origins:
        handlers = [
            h for h in KEY_LOOKUP_HANDLERS
            if (h.NAME in origins) or (h.NAME.lower() in origins)
        ]
    else:
        handlers = KEY_LOOKUP_HANDLERS

    ungotten = get and get[:] or []
    progress = []

    for handler in handlers:
        if get and not ungotten:
            # We have all the keys!
            break

        try:
            h = handler(session, known_keys_list)
            if not allowremote and not h.LOCAL:
                continue

            if found_keys and (not h.PRIVACY_FRIENDLY) and (not origins):
                # We only try the privacy-hostile methods if we haven't
                # found any keys (unless origins were specified).
                if not ungotten:
                    continue

            progress.append(h.NAME)
            if event:
                ordered_keys.sort(key=lambda k: -k["score"])
                event.message = _('Searching for encryption keys in: %s') % _(
                    h.NAME)
                event.private_data = {
                    "result": ordered_keys,
                    "progress": progress,
                    "runningsearch": h.NAME
                }
                session.config.event_log.log_event(event)

            # We allow for more time when importing keys
            timeout = h.TIMEOUT
            if ungotten:
                timeout *= 4

            # h.lookup will remove found keys from the wanted list,
            # but we have to watch out for the effects of timeouts.
            wanted = ungotten[:]
            results = RunTimed(timeout,
                               h.lookup,
                               address,
                               strict_email_match=strict_email_match,
                               get=(wanted if (get is not None) else None))
            ungotten[:] = wanted
        except KeyboardInterrupt:
            raise
        except:
            if session.config.sys.debug:
                traceback.print_exc()
            results = {}

        for key_id, key_info in results.iteritems():
            if key_id in found_keys:
                old_scores = found_keys[key_id].get('scores', {})
                old_uids = found_keys[key_id].get('uids', [])[:]
                found_keys[key_id].update(key_info)
                if 'scores' in found_keys[key_id]:
                    found_keys[key_id]['scores'].update(old_scores)
                    # No need for an else, as old_scores will be empty

                # Merge in the old UIDs
                uid_emails = [u['email'] for u in key_info.get('uids', [])]
                if 'uids' not in found_keys[key_id]:
                    found_keys[key_id]['uids'] = []
                for uid in old_uids:
                    email = uid.get('email')
                    if email and email not in uid_emails:
                        found_keys[key_id]['uids'].append(uid)
            else:
                found_keys[key_id] = key_info
                found_keys[key_id]["origins"] = []
            found_keys[key_id]["origins"].append(h.NAME)
            _update_scores(session, key_id, found_keys[key_id],
                           known_keys_list)
            _normalize_key(session, found_keys[key_id])

        # This updates and sorts ordered_keys in place. This will magically
        # also update the data on the viewable event, because Python.
        ordered_keys[:] = found_keys.values()
        ordered_keys.sort(key=lambda k: -k["score"])

    if event:
        event.private_data = {"result": ordered_keys, "runningsearch": False}
        session.config.event_log.log_event(event)
    return ordered_keys
Beispiel #43
0
 def _score(self, key):
     return (self.SCORE, _('Found encryption key in keychain'))
Beispiel #44
0
 def _score(self, key):
     return (self.SCORE, _('Found encryption key in keys.openpgp.org'))
Beispiel #45
0
    def open(self, conn_cls=None, throw=False):
        conn = self.conn
        conn_id = self._conn_id()
        if conn:
            try:
                with conn as c:
                    if (conn_id == self.conn_id
                            and self.timed(c.noop)[0] == 'OK'):
                        # Make the timeout longer, so we don't drop things
                        # on every hiccup and so downloads will be more
                        # efficient (chunk size relates to timeout).
                        self.timeout = self.TIMEOUT_LIVE
                        return conn
            except self.CONN_ERRORS + (AttributeError, ):
                pass
            with self._lock:
                if self.conn == conn:
                    self.conn = None
            conn.quit()

        # This facilitates testing, event should already exist in real life.
        if self.event:
            event = self.event
        else:
            event = Event(source=self, flags=Event.RUNNING, data={})

        # 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')]
        }

        conn = None
        my_config = self.my_config
        mailboxes = my_config.mailbox.values()

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

        try:

            def mkconn():
                with ConnBroker.context(need=[ConnBroker.OUTGOING_IMAP]):
                    return conn_cls(my_config.host, my_config.port)

            conn = self.timed(mkconn)
            conn.debug = ('imaplib'
                          in self.session.config.sys.debug) and 4 or 0

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

            #if 'STARTTLS' in capabilities and not want_ssl:
            #
            # FIXME: We need to send a STARTTLS and do a switcheroo where
            #        the connection gets encrypted.

            try:
                ok, data = self.timed_imap(conn.login, my_config.username,
                                           my_config.password)
            except IMAP4.error:
                ok = False
            if not ok:
                ev['error'] = ['auth', _('Invalid username or password')]
                if throw:
                    raise throw(event.data['conn_error'])
                return WithaBool(False)

            with self._lock:
                if self.conn is not None:
                    raise IOError('Woah, we lost a race.')
                self.capabilities = capabilities
                if 'IDLE' in capabilities:
                    self.conn = SharedImapConn(
                        self.session,
                        conn,
                        idle_mailbox='INBOX',
                        idle_callback=self._idle_callback)
                else:
                    self.conn = SharedImapConn(self.session, conn)

            if self.event:
                self._log_status(
                    _('Connected to IMAP server %s') % my_config.host)
            if 'imap' in self.session.config.sys.debug:
                self.session.ui.debug('CONNECTED %s' % self.conn)

            self.conn_id = conn_id
            ev['live'] = True
            return self.conn

        except TimedOut:
            if 'imap' in self.session.config.sys.debug:
                self.session.ui.debug(traceback.format_exc())
            ev['error'] = ['timeout', _('Connection timed out')]
        except (IMAP_IOError, IMAP4.error):
            if 'imap' in self.session.config.sys.debug:
                self.session.ui.debug(traceback.format_exc())
            ev['error'] = ['protocol', _('An IMAP protocol error occurred')]
        except (IOError, AttributeError, socket.error):
            if 'imap' in self.session.config.sys.debug:
                self.session.ui.debug(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 WithaBool(False)
Beispiel #46
0
 def command(self):
     self.session.ui.display_result(
         HelpSplash(self.session, 'help', []).run(interactive=False))
     while not mailpile.util.QUITTING:
         time.sleep(1)
     return self._success(_('Did nothing much for a while'))
Beispiel #47
0
 def add(self, message):
     raise Exception('FIXME: Need to RETURN AN ID.')
     with self.open_imap() as imap:
         ok, data = self.timed_imap(imap.append, self.path, message=message)
         self._assert(ok, _('Failed to add message'))
Beispiel #48
0
                              [self.event])],
                            test_only=True, test_route=route))
            return self._success(_('Route is working'),
                                 result=route)
        except OSError:
            error_info = {'error': _('Invalid command'),
                          'invalid_command': True}
        except SendMailError, e:
            error_info = {'error': e.message,
                          'sendmail_error': True}
            error_info.update(e.error_info)
        except:
            import traceback
            traceback.print_exc()

        return self._error(_('Route is not working'),
                           result=route, info=error_info)


class Setup(TestableWebbable):
    """Enter setup flow"""
    SYNOPSIS = (None, 'setup', 'setup', '[do_gpg_stuff]')

    ORDER = ('Internals', 0)
    LOG_PROGRESS = True
    HTTP_CALLABLE = ('GET',)
    HTTP_AUTH_REQUIRED = True

    # These are a global, may be modified...
    KEY_WORKER_LOCK = CryptoRLock()
    KEY_CREATING_THREAD = None
Beispiel #49
0
    def command(self, emails=None):
        session, idx = self.session, self._idx()
        args = list(self.args)

        files = []
        filedata = {}
        if 'file-data' in self.data:
            count = 0
            for fd in self.data['file-data']:
                fn = (hasattr(fd, 'filename') and fd.filename
                      or 'attach-%d.dat' % count)
                filedata[fn] = fd
                files.append(fn)
                count += 1
        else:
            while os.path.exists(args[-1]):
                # Attaching from the local filesystem is scary!
                if self.session.config.sys.lockdown:
                    return self._error(_('In lockdown, doing nothing.'))
                files.append(args.pop(-1))

        if not files:
            return self._error(_('No files found'))

        if not emails:
            args.extend(['=%s' % mid for mid in self.data.get('mid', [])])
            emails = [
                self._actualize_ephemeral(i)
                for i in self._choose_messages(args, allow_ephemeral=True)
            ]
        if not emails:
            return self._error(_('No messages selected'))

        updated = []
        errors = []

        def err(msg):
            errors.append(msg)
            session.ui.error(msg)

        for email in emails:
            subject = email.get_msg_info(MailIndex.MSG_SUBJECT)
            try:
                email.add_attachments(session, files, filedata=filedata)
                updated.append(email)
            except NotEditableError:
                err(_('Read-only message: %s') % subject)
            except:
                err(_('Error attaching to %s') % subject)
                self._ignore_exception()

        if errors:
            self.message = _('Attached %s to %d messages, failed %d') % (
                ', '.join(files), len(updated), len(errors))
        else:
            self.message = _('Attached %s to %d messages') % (', '.join(files),
                                                              len(updated))

        session.ui.notify(self.message)
        return self._return_search_results(self.message,
                                           updated,
                                           expand=updated,
                                           error=errors)
Beispiel #50
0
 def _edit_messages(self, *args, **kwargs):
     try:
         return self._real_edit_messages(*args, **kwargs)
     except NotEditableError:
         return self._error(_('Message is not editable'))
Beispiel #51
0
    def CreateReply(cls,
                    idx,
                    session,
                    refs,
                    msgid,
                    reply_all=False,
                    cid=None,
                    ephemeral=False):
        trees = [
            m.evaluate_pgp(m.get_message_tree(), decrypt=True) for m in refs
        ]

        headers = cls._create_from_to_cc(idx, session, trees)
        if not reply_all and 'cc' in headers:
            del headers['cc']

        ref_ids = [t['headers_lc'].get('message-id') for t in trees]
        ref_subjs = [t['headers_lc'].get('subject') for t in trees]
        msg_bodies = []
        for t in trees:
            # FIXME: Templates/settings for how we quote replies?
            quoted = ''.join([
                p['data'] for p in t['text_parts']
                if p['type'] in cls._TEXT_PARTTYPES and p['data']
            ])
            if quoted:
                text = ((_('%s wrote:') % t['headers_lc']['from']) + '\n' +
                        split_long_lines(quoted))
                msg_bodies.append('\n\n' + text.replace('\n', '\n> '))

        if not ephemeral:
            local_id, lmbox = session.config.open_local_mailbox(session)
        else:
            local_id, lmbox = -1, None
            fmt = 'reply-all-%s-%s' if reply_all else 'reply-%s-%s'
            ephemeral = [
                fmt % (msgid[1:-1].replace('@', '_'), refs[0].msg_mid())
            ]

        if 'cc' in headers:
            fmt = _('Composing a reply from %(from)s to %(to)s, cc %(cc)s')
        else:
            fmt = _('Composing a reply from %(from)s to %(to)s')
        session.ui.debug(fmt % headers)

        if cid:
            # FIXME: Instead, we should use placeholders in the template
            #        and insert the quoted bits in the right place (or
            #        nowhere if the template doesn't want them).
            msg_bodies[:0] = [cls._get_canned(idx, cid)]

        return (Email.Create(idx,
                             local_id,
                             lmbox,
                             msg_text='\n\n'.join(msg_bodies),
                             msg_subject=('Re: %s' % ref_subjs[-1]),
                             msg_from=headers.get('from', None),
                             msg_to=headers.get('to', []),
                             msg_cc=headers.get('cc', []),
                             msg_references=[i for i in ref_ids if i],
                             msg_id=msgid,
                             save=(not ephemeral),
                             ephemeral_mid=ephemeral
                             and ephemeral[0]), ephemeral)
Beispiel #52
0
    def command(self, emails=None):
        session, config, idx = self.session, self.session.config, self._idx()
        args = list(self.args)

        if self.session.config.sys.lockdown:
            return self._error(_('In lockdown, doing nothing.'))

        bounce_to = []
        while args and '@' in args[-1]:
            bounce_to.append(args.pop(-1))
        for rcpt in (self.data.get('to', []) + self.data.get('cc', []) +
                     self.data.get('bcc', [])):
            bounce_to.extend(ExtractEmails(rcpt))

        if not emails:
            args.extend(['=%s' % mid for mid in self.data.get('mid', [])])
            mids = self._choose_messages(args)
            emails = [Email(idx, i) for i in mids]

        # Process one at a time so we don't eat too much memory
        sent = []
        missing_keys = []
        for email in emails:
            events = []
            try:
                msg_mid = email.get_msg_info(idx.MSG_MID)

                # This is a unique sending-ID. This goes in the public (meant
                # for debugging help) section of the event-log, so we take
                # care to not reveal details about the message or recipients.
                msg_sid = sha1b64(email.get_msg_info(idx.MSG_ID),
                                  *sorted(bounce_to))[:8]

                # We load up any incomplete events for sending this message
                # to this set of recipients. If nothing is in flight, create
                # a new event for tracking this operation.
                events = list(
                    config.event_log.incomplete(source=self.EVENT_SOURCE,
                                                data_mid=msg_mid,
                                                data_sid=msg_sid))
                if not events:
                    events.append(
                        config.event_log.log(source=self.EVENT_SOURCE,
                                             flags=Event.RUNNING,
                                             message=_('Sending message'),
                                             data={
                                                 'mid': msg_mid,
                                                 'sid': msg_sid
                                             }))

                SendMail(session, msg_mid, [
                    PrepareMessage(config,
                                   email.get_msg(pgpmime=False),
                                   rcpts=(bounce_to or None),
                                   events=events)
                ])
                for ev in events:
                    ev.flags = Event.COMPLETE
                    config.event_log.log_event(ev)
                sent.append(email)
            except KeyLookupError, kle:
                # This is fatal, we don't retry
                message = _('Missing keys %s') % kle.missing
                for ev in events:
                    ev.flags = Event.COMPLETE
                    ev.message = message
                    config.event_log.log_event(ev)
                session.ui.warning(message)
                missing_keys.extend(kle.missing)
                self._ignore_exception()
            # FIXME: Also fatal, when the SMTP server REJECTS the mail
            except:
Beispiel #53
0
                    config.event_log.log_event(ev)
                sent.append(email)
            except KeyLookupError, kle:
                # This is fatal, we don't retry
                message = _('Missing keys %s') % kle.missing
                for ev in events:
                    ev.flags = Event.COMPLETE
                    ev.message = message
                    config.event_log.log_event(ev)
                session.ui.warning(message)
                missing_keys.extend(kle.missing)
                self._ignore_exception()
            # FIXME: Also fatal, when the SMTP server REJECTS the mail
            except:
                # We want to try that again!
                message = _('Failed to send %s') % email
                for ev in events:
                    ev.flags = Event.INCOMPLETE
                    ev.message = message
                    config.event_log.log_event(ev)
                session.ui.error(message)
                self._ignore_exception()

        if 'compose' in config.sys.debug:
            sys.stderr.write(
                ('compose/Sendit: Send %s to %s (sent: %s)\n') %
                (len(emails), bounce_to or '(header folks)', sent))

        if missing_keys:
            self.error_info['missing_keys'] = missing_keys
        if sent:
Beispiel #54
0
        def add_rule(self, key, rule):
            if not ((isinstance(rule, (list, tuple))) and
                    (key == CleanText(key, banned=CleanText.NONVARS).clean) and
                    (not self.real_hasattr(key))):
                raise TypeError('add_rule(%s, %s): Bad key or rule.' %
                                (key, rule))

            orule, rule = rule, ConfigRule(*rule[:])
            if hasattr(orule, '_types'):
                rule._types = orule._types

            self.rules[key] = rule
            check = rule[self.RULE_CHECKER]
            try:
                check = self.RULE_CHECK_MAP.get(check, check)
                rule[self.RULE_CHECKER] = check
            except TypeError:
                pass

            name = '%s/%s' % (self._name, key)
            comment = rule[self.RULE_COMMENT]
            value = rule[self.RULE_DEFAULT]
            ww = self.real_getattr('_write_watcher')

            if (isinstance(check, dict) and value is not None
                    and not isinstance(value, (dict, list))):
                raise TypeError(
                    _('Only lists or dictionaries can contain '
                      'dictionary values (key %s).') % name)

            if isinstance(value, dict) and check is False:
                pcls.__setitem__(
                    self, key,
                    ConfigDict(_name=name,
                               _comment=comment,
                               _write_watcher=ww,
                               _rules=value))

            elif isinstance(value, dict):
                if value:
                    raise ConfigValueError(
                        _('Subsections must be immutable '
                          '(key %s).') % name)
                sub_rule = {'_any': [rule[self.RULE_COMMENT], check, None]}
                checker = _MakeCheck(ConfigDict, name, check, sub_rule, ww)
                pcls.__setitem__(self, key, checker())
                rule[self.RULE_CHECKER] = checker

            elif isinstance(value, list):
                if value:
                    raise ConfigValueError(
                        _('Lists cannot have default '
                          'values (key %s).') % name)
                sub_rule = {'_any': [rule[self.RULE_COMMENT], check, None]}
                checker = _MakeCheck(ConfigList, name, comment, sub_rule, ww)
                pcls.__setitem__(self, key, checker())
                rule[self.RULE_CHECKER] = checker

            elif not isinstance(
                    value, (type(None), int, long, bool, float, str, unicode)):
                raise TypeError(
                    _('Invalid type "%s" for key "%s" (value: %s)') %
                    (type(value), name, repr(value)))
Beispiel #55
0
    def parse_config(self, session, data, source='internal'):
        """
        Parse a config file fragment. Invalid data will be ignored, but will
        generate warnings in the session UI. Returns True on a clean parse,
        False if any of the settings were bogus.

        >>> cfg.parse_config(session, '[config/sys]\\nfd_cache_size = 123\\n')
        True
        >>> cfg.sys.fd_cache_size
        123

        >>> cfg.parse_config(session, '[config/bogus]\\nblabla = bla\\n')
        False
        >>> [l[1] for l in session.ui.log_buffer if 'bogus' in l[1]][0]
        'Invalid (internal): section config/bogus does not exist'

        >>> cfg.parse_config(session, '[config/sys]\\nhistory_length = 321\\n'
        ...                                          'bogus_variable = 456\\n')
        False
        >>> cfg.sys.history_length
        321
        >>> [l[1] for l in session.ui.log_buffer if 'bogus_var' in l[1]][0]
        u'Invalid (internal): section config/sys, ...
        """
        parser = CommentedEscapedConfigParser()
        parser.readfp(io.BytesIO(str(data)))

        def item_sorter(i):
            try:
                return (int(i[0], 36), i[1])
            except (ValueError, IndexError, KeyError, TypeError):
                return i

        all_okay = True
        for section in parser.sections():
            okay = True
            cfgpath = section.split(':')[0].split('/')[1:]
            cfg = self
            added_parts = []
            for part in cfgpath:
                if cfg.fmt_key(part) in cfg.keys():
                    cfg = cfg[part]
                elif '_any' in cfg.rules:
                    cfg[part] = {}
                    cfg = cfg[part]
                else:
                    if session:
                        msg = _('Invalid (%s): section %s does not '
                                'exist') % (source, section)
                        session.ui.warning(msg)
                    all_okay = okay = False
            items = parser.items(section) if okay else []
            items.sort(key=item_sorter)
            for var, val in items:
                try:
                    cfg[var] = val
                except (ValueError, KeyError, IndexError):
                    if session:
                        msg = _(u'Invalid (%s): section %s, variable %s=%s'
                                ) % (source, section, var, val)
                        session.ui.warning(msg)
                    all_okay = okay = False
        return all_okay
Beispiel #56
0
    def _get_email_updates(self, idx, create=False, noneok=False, emails=None):
        # Split the argument list into files and message IDs
        files = [f[1:].strip() for f in self.args if f.startswith('<')]
        args = [a for a in self.args if not a.startswith('<')]

        # Message IDs can come from post data
        for mid in self.data.get('mid', []):
            args.append('=%s' % mid)
        emails = emails or [
            self._actualize_ephemeral(mid)
            for mid in self._choose_messages(args, allow_ephemeral=True)
        ]

        update_header_set = (set(self.data.keys())
                             & set([k.lower() for k in self.UPDATE_HEADERS]))
        updates, fofs = [], 0
        for e in (emails or (create and [None]) or []):
            # If we don't have a file, check for posted data
            if len(files) not in (0, 1, len(emails)):
                return (self._error(_('Cannot update from multiple files')),
                        None)
            elif len(files) == 1:
                updates.append((e, self._read_file_or_data(files[0])))
            elif files and (len(files) == len(emails)):
                updates.append((e, self._read_file_or_data(files[fofs])))
            elif update_header_set:
                # No file name, construct an update string from the POST data.
                etree = e and e.get_message_tree() or {}
                defaults = etree.get('editing_strings', {})

                up = []
                for hdr in self.UPDATE_HEADERS:
                    if hdr.lower() in self.data:
                        data = ', '.join(self.data[hdr.lower()])
                    else:
                        data = defaults.get(hdr.lower(), '')
                    up.append('%s: %s' % (hdr, data))

                # This preserves in-reply-to, references and any other
                # headers we're not usually keen on editing.
                if defaults.get('headers'):
                    up.append(defaults['headers'])

                # This weird thing converts attachment=1234:bla.txt into a
                # dict of 1234=>bla.txt values, attachment=1234 to 1234=>None.
                # .. or just keeps all attachments if nothing is specified.
                att_keep = (dict([(ai.split(':', 1) if (':' in ai) else
                                   (ai, None))
                                  for ai in self.data.get('attachment', [])])
                            if 'attachment' in self.data else defaults.get(
                                'attachments', {}))
                for att_id, att_fn in defaults.get('attachments',
                                                   {}).iteritems():
                    if att_id in att_keep:
                        fn = att_keep[att_id] or att_fn
                        up.append('Attachment-%s: %s' % (att_id, fn))

                updates.append((e, '\n'.join(up + [
                    '', '\n'.join(
                        self.data.get('body', defaults.get('body', '')))
                ])))
            elif noneok:
                updates.append((e, None))
            elif 'compose' in self.session.config.sys.debug:
                sys.stderr.write('Doing nothing with %s' % update_header_set)
            fofs += 1

        if 'compose' in self.session.config.sys.debug:
            for e, up in updates:
                sys.stderr.write(
                    ('compose/update: Update %s with:\n%s\n--\n') %
                    ((e and e.msg_mid() or '(new'), up))
            if not updates:
                sys.stderr.write('compose/update: No updates!\n')

        return updates
Beispiel #57
0
class SetupMagic(Command):
    """Perform initial setup"""
    SYNOPSIS = (None, None, None, None)
    ORDER = ('Internals', 0)
    LOG_PROGRESS = True

    TAGS = {
        'New': {
            'type': 'unread',
            'label': False,
            'display': 'invisible',
            'icon': 'icon-new',
            'label_color': '03-gray-dark',
            'name': _('New'),
        },
        'Inbox': {
            'type': 'inbox',
            'display': 'priority',
            'display_order': 2,
            'icon': 'icon-inbox',
            'label_color': '06-blue',
            'name': _('Inbox'),
        },
        'Blank': {
            'type': 'blank',
            'flag_editable': True,
            'display': 'invisible',
            'name': _('Blank'),
        },
        'Drafts': {
            'type': 'drafts',
            'flag_editable': True,
            'display': 'priority',
            'display_order': 1,
            'icon': 'icon-compose',
            'label_color': '03-gray-dark',
            'name': _('Drafts'),
        },
        'Outbox': {
            'type': 'outbox',
            'display': 'priority',
            'display_order': 3,
            'icon': 'icon-outbox',
            'label_color': '06-blue',
            'name': _('Outbox'),
        },
        'Sent': {
            'type': 'sent',
            'display': 'priority',
            'display_order': 4,
            'icon': 'icon-sent',
            'label_color': '03-gray-dark',
            'name': _('Sent'),
        },
        'Spam': {
            'type': 'spam',
            'flag_hides': True,
            'display': 'priority',
            'display_order': 5,
            'icon': 'icon-spam',
            'label_color': '10-orange',
            'name': _('Spam'),
        },
        'MaybeSpam': {
            'display': 'invisible',
            'icon': 'icon-spam',
            'label_color': '10-orange',
            'name': _('MaybeSpam'),
        },
        'Ham': {
            'type': 'ham',
            'display': 'invisible',
            'name': _('Ham'),
        },
        'Trash': {
            'type': 'trash',
            'flag_hides': True,
            'display': 'priority',
            'display_order': 6,
            'icon': 'icon-trash',
            'label_color': '13-brown',
            'name': _('Trash'),
        },
        # These are magical tags that perform searches and show
        # messages in contextual views.
        'All Mail': {
            'type': 'tag',
            'icon': 'icon-logo',
            'label_color': '06-blue',
            'search_terms': 'all:mail',
            'name': _('All Mail'),
            'display_order': 1000,
        },
        'Photos': {
            'type': 'tag',
            'icon': 'icon-photos',
            'label_color': '08-green',
            'search_terms': 'att:jpg',
            'name': _('Photos'),
            'template': 'photos',
            'display_order': 1001,
        },
        'Files': {
            'type': 'tag',
            'icon': 'icon-document',
            'label_color': '06-blue',
            'search_terms': 'has:attachment',
            'name': _('Files'),
            'template': 'files',
            'display_order': 1002,
        },
        'Links': {
            'type': 'tag',
            'icon': 'icon-links',
            'label_color': '12-red',
            'search_terms': 'http',
            'name': _('Links'),
            'display_order': 1003,
        },
        # These are internal tags, used for tracking user actions on
        # messages, as input for machine learning algorithms. These get
        # automatically added, and may be automatically removed as well
        # to keep the working sets reasonably small.
        'mp_rpl': {
            'type': 'replied',
            'label': False,
            'display': 'invisible'
        },
        'mp_fwd': {
            'type': 'fwded',
            'label': False,
            'display': 'invisible'
        },
        'mp_tag': {
            'type': 'tagged',
            'label': False,
            'display': 'invisible'
        },
        'mp_read': {
            'type': 'read',
            'label': False,
            'display': 'invisible'
        },
        'mp_ham': {
            'type': 'ham',
            'label': False,
            'display': 'invisible'
        },
    }

    def basic_app_config(self,
                         session,
                         save_and_update_workers=True,
                         want_daemons=True):
        # Create local mailboxes
        session.config.open_local_mailbox(session)

        # Create standard tags and filters
        created = []
        for t in self.TAGS:
            if not session.config.get_tag_id(t):
                AddTag(session, arg=[t]).run(save=False)
                created.append(t)
            session.config.get_tag(t).update(self.TAGS[t])
        for stype, statuses in (('sig', SignatureInfo.STATUSES),
                                ('enc', EncryptionInfo.STATUSES)):
            for status in statuses:
                tagname = 'mp_%s-%s' % (stype, status)
                if not session.config.get_tag_id(tagname):
                    AddTag(session, arg=[tagname]).run(save=False)
                    created.append(tagname)
                session.config.get_tag(tagname).update({
                    'type': 'attribute',
                    'display': 'invisible',
                    'label': False,
                })

        if 'New' in created:
            session.ui.notify(_('Created default tags'))

        # Import all the basic plugins
        reload_config = False
        for plugin in PLUGINS:
            if plugin not in session.config.sys.plugins:
                session.config.sys.plugins.append(plugin)
                reload_config = True
        if reload_config:
            with session.config._lock:
                session.config.save()
                session.config.load(session)

        try:
            # If spambayes is not installed, this will fail
            import mailpile.plugins.autotag_sb
            if 'autotag_sb' not in session.config.sys.plugins:
                session.config.sys.plugins.append('autotag_sb')
                session.ui.notify(_('Enabling spambayes autotagger'))
        except ImportError:
            session.ui.warning(
                _('Please install spambayes '
                  'for super awesome spam filtering'))

        vcard_importers = session.config.prefs.vcard.importers
        if not vcard_importers.gravatar:
            vcard_importers.gravatar.append({'active': True})
            session.ui.notify(_('Enabling gravatar image importer'))

        gpg_home = os.path.expanduser('~/.gnupg')
        if os.path.exists(gpg_home) and not vcard_importers.gpg:
            vcard_importers.gpg.append({'active': True, 'gpg_home': gpg_home})
            session.ui.notify(_('Importing contacts from GPG keyring'))

        if ('autotag_sb' in session.config.sys.plugins
                and len(session.config.prefs.autotag) == 0):
            session.config.prefs.autotag.append({
                'match_tag': 'spam',
                'unsure_tag': 'maybespam',
                'tagger': 'spambayes',
                'trainer': 'spambayes'
            })
            session.config.prefs.autotag[0].exclude_tags[0] = 'ham'

        if save_and_update_workers:
            session.config.save()
            session.config.prepare_workers(session, daemons=want_daemons)

    def setup_command(self, session, do_gpg_stuff=False):
        do_gpg_stuff = do_gpg_stuff or ('do_gpg_stuff' in self.args)

        # Stop the workers...
        want_daemons = session.config.cron_worker is not None
        session.config.stop_workers()

        # Perform any required migrations
        Migrate(session).run(before_setup=True, after_setup=False)

        # Basic app config, tags, plugins, etc.
        self.basic_app_config(session,
                              save_and_update_workers=False,
                              want_daemons=want_daemons)

        # Assumption: If you already have secret keys, you want to
        #             use the associated addresses for your e-mail.
        #             If you don't already have secret keys, you should have
        #             one made for you, if GnuPG is available.
        #             If GnuPG is not available, you should be warned.
        if do_gpg_stuff:
            gnupg = GnuPG()
            accepted_keys = []
            if gnupg.is_available():
                keys = gnupg.list_secret_keys()
                for key, details in keys.iteritems():
                    # Ignore revoked/expired keys.
                    if ("revocation-date" in details
                            and details["revocation-date"] <=
                            date.today().strftime("%Y-%m-%d")):
                        continue

                    accepted_keys.append(key)
                    for uid in details["uids"]:
                        if "email" not in uid or uid["email"] == "":
                            continue

                        if uid["email"] in [
                                x["email"] for x in session.config.profiles
                        ]:
                            # Don't set up the same e-mail address twice.
                            continue

                        # FIXME: Add route discovery mechanism.
                        profile = {
                            "email": uid["email"],
                            "name": uid["name"],
                        }
                        session.config.profiles.append(profile)
                    if (session.config.prefs.gpg_recipient
                            in (None, '', '!CREATE')
                            and details["capabilities_map"][0]["encrypt"]):
                        session.config.prefs.gpg_recipient = key
                        session.ui.notify(_('Encrypting config to %s') % key)
                    if session.config.prefs.crypto_policy == 'none':
                        session.config.prefs.crypto_policy = 'openpgp-sign'

                if len(accepted_keys) == 0:
                    # FIXME: Start background process generating a key once a user
                    #        has supplied a name and e-mail address.
                    pass

            else:
                session.ui.warning(_('Oh no, PGP/GPG support is unavailable!'))

        # If we have a GPG key, but no master key, create it
        self.make_master_key()

        # Perform any required migrations
        Migrate(session).run(before_setup=False, after_setup=True)

        session.config.save()
        session.config.prepare_workers(session, daemons=want_daemons)

        return self._success(_('Performed initial Mailpile setup'))

    def make_master_key(self):
        session = self.session
        if (session.config.prefs.gpg_recipient not in (None, '', '!CREATE')
                and not session.config.master_key
                and not session.config.prefs.obfuscate_index):
            #
            # This secret is arguably the most critical bit of data in the
            # app, it is used as an encryption key and to seed hashes in
            # a few places.  As such, the user may need to type this in
            # manually as part of data recovery, so we keep it reasonably
            # sized and devoid of confusing chars.
            #
            # The strategy below should give about 281 bits of randomness:
            #
            #   import math
            #   math.log((25 + 25 + 8) ** (12 * 4), 2) == 281.183...
            #
            secret = ''
            chars = 12 * 4
            while len(secret) < chars:
                secret = sha512b64(os.urandom(1024), '%s' % session.config,
                                   '%s' % time.time())
                secret = CleanText(secret, banned=CleanText.NONALNUM +
                                   'O01l').clean[:chars]
            session.config.master_key = secret
            if self._idx() and self._idx().INDEX:
                session.ui.warning(
                    _('Unable to obfuscate search index '
                      'without losing data. Not indexing '
                      'encrypted mail.'))
            else:
                session.config.prefs.obfuscate_index = True
                session.config.prefs.index_encrypted = True
                session.ui.notify(
                    _('Obfuscating search index and enabling '
                      'indexing of encrypted e-mail. Yay!'))
            return True
        else:
            return False

    def command(self, *args, **kwargs):
        session = self.session
        if session.config.sys.lockdown:
            return self._error(_('In lockdown, doing nothing.'))
        return self.setup_command(session, *args, **kwargs)
Beispiel #58
0
 def command(self, *args, **kwargs):
     session = self.session
     if session.config.sys.lockdown:
         return self._error(_('In lockdown, doing nothing.'))
     return self.setup_command(session, *args, **kwargs)
Beispiel #59
0
    def setup_command(self, session, do_gpg_stuff=False):
        do_gpg_stuff = do_gpg_stuff or ('do_gpg_stuff' in self.args)

        # Stop the workers...
        want_daemons = session.config.cron_worker is not None
        session.config.stop_workers()

        # Perform any required migrations
        Migrate(session).run(before_setup=True, after_setup=False)

        # Basic app config, tags, plugins, etc.
        self.basic_app_config(session,
                              save_and_update_workers=False,
                              want_daemons=want_daemons)

        # Assumption: If you already have secret keys, you want to
        #             use the associated addresses for your e-mail.
        #             If you don't already have secret keys, you should have
        #             one made for you, if GnuPG is available.
        #             If GnuPG is not available, you should be warned.
        if do_gpg_stuff:
            gnupg = GnuPG()
            accepted_keys = []
            if gnupg.is_available():
                keys = gnupg.list_secret_keys()
                for key, details in keys.iteritems():
                    # Ignore revoked/expired keys.
                    if ("revocation-date" in details
                            and details["revocation-date"] <=
                            date.today().strftime("%Y-%m-%d")):
                        continue

                    accepted_keys.append(key)
                    for uid in details["uids"]:
                        if "email" not in uid or uid["email"] == "":
                            continue

                        if uid["email"] in [
                                x["email"] for x in session.config.profiles
                        ]:
                            # Don't set up the same e-mail address twice.
                            continue

                        # FIXME: Add route discovery mechanism.
                        profile = {
                            "email": uid["email"],
                            "name": uid["name"],
                        }
                        session.config.profiles.append(profile)
                    if (session.config.prefs.gpg_recipient
                            in (None, '', '!CREATE')
                            and details["capabilities_map"][0]["encrypt"]):
                        session.config.prefs.gpg_recipient = key
                        session.ui.notify(_('Encrypting config to %s') % key)
                    if session.config.prefs.crypto_policy == 'none':
                        session.config.prefs.crypto_policy = 'openpgp-sign'

                if len(accepted_keys) == 0:
                    # FIXME: Start background process generating a key once a user
                    #        has supplied a name and e-mail address.
                    pass

            else:
                session.ui.warning(_('Oh no, PGP/GPG support is unavailable!'))

        # If we have a GPG key, but no master key, create it
        self.make_master_key()

        # Perform any required migrations
        Migrate(session).run(before_setup=False, after_setup=True)

        session.config.save()
        session.config.prepare_workers(session, daemons=want_daemons)

        return self._success(_('Performed initial Mailpile setup'))
Beispiel #60
0
 def reset(self, rules=True, data=True):
     raise Exception(_('Please override this method'))