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))
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)
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!'))
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)
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 })
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)
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)
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})
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'))
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
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))
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
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)
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)
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`", ]
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
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))
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 })
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
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
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)])
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'))
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)
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)
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)
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])
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))
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)
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
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
def _lockdown_strict(config): if DISABLE_LOCKDOWN: return False if _lockdown(config) > 1: return _('In lockdown, doing nothing.') return in_disk_lockdown(config)
def setup_command(self, session): # FIXME! return self._success(_('Configuring a key'), self._result())
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)
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'),
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
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
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] })
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
def _lockdown_minimal(config): if DISABLE_LOCKDOWN: return False if _lockdown(config) != 0: return _('In lockdown, doing nothing.') return False
def _score(self, key): return (self.SCORE, _('Found encryption key in keyserver'))
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
def _score(self, key): return (self.SCORE, _('Found encryption key in keychain'))
def _score(self, key): return (self.SCORE, _('Found encryption key in keys.openpgp.org'))
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)
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'))
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'))
[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
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)
def _edit_messages(self, *args, **kwargs): try: return self._real_edit_messages(*args, **kwargs) except NotEditableError: return self._error(_('Message is not editable'))
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)
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:
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:
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)))
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
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
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)
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)
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 reset(self, rules=True, data=True): raise Exception(_('Please override this method'))