def _do_tagging(self, ops, msg_ids, conversations, save=True, auto=False): idx = self._idx() rv = {"conversations": False, "msg_ids": [b36(i) for i in msg_ids], "tagged": [], "untagged": []} for op in ops: tag = self.session.config.get_tag(op[1:]) if tag: # FIXME: This should depend on more factors! # - Tags should have metadata about default scope if conversations is None: conversation = "flat" not in (self.session.order or "") if tag.flag_msg_only or tag.flag_editable or tag.type == "attribute": conversation = False else: conversation = conversations if conversation: rv["conversations"] = True tag_id = tag._key tag = tag.copy() tag["tid"] = tag_id if op[0] == "-": removed = idx.remove_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv["untagged"].append((tag, sorted([b36(i) for i in removed]))) else: added = idx.add_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv["tagged"].append((tag, sorted([b36(i) for i in added]))) # Record behavior if len(msg_ids) < 15: for t in self.session.config.get_tags(type="tagged"): idx.add_tag(self.session, t._key, msg_idxs=msg_ids) else: self.session.ui.warning("Unknown tag: %s" % op) if rv["conversations"]: undo_msg = _n("Untag %d conversation", "Untag %d conversations", len(msg_ids)) % len(msg_ids) done_msg = _n("Tagged %d conversation", "Tagged %d conversations", len(msg_ids)) % len(msg_ids) else: undo_msg = _n("Untag %d message", "Untag %d messages", len(msg_ids)) % len(msg_ids) done_msg = _n("Tagged %d message", "Tagged %d messages", len(msg_ids)) % len(msg_ids) self.event.data["undo"] = undo_msg self.event.private_data["undo"] = { "tagged": [[t["tid"], mids] for t, mids in rv["tagged"]], "untagged": [[t["tid"], mids] for t, mids in rv["untagged"]], } self.finish(save=save) return self._success(done_msg, rv)
def command(self): args = list(self.args) if len(args) > 1: allowremote = args.pop() else: allowremote = self.data.get('allowremote', ['Y'])[0] if allowremote.lower()[:1] in ('n', 'f'): allowremote = False origins = self.data.get('origins') if '*' in (origins or []): origins = [h.NAME for h in KEY_LOOKUP_HANDLERS] email = " ".join(self.data.get('email', [])) address = " ".join(self.data.get('address', args)) result = lookup_crypto_keys(self.session, email or address, strict_email_match=email, event=self.event, allowremote=allowremote, origins=origins) return self._success( _n('Found %d encryption key', 'Found %d encryption keys', len(result)) % len(result), result=result)
def command(self): args = list(self.args) if args: address, fprints, origins = args[0], args[1].split(','), args[2:] else: address = self.data.get('address', [''])[0] fprints = self.data.get('fingerprints', []) origins = self.data.get('origins', []) safe_assert(address or fprints or origins) result = lookup_crypto_keys(self.session, address, get=[f.strip() for f in fprints], origins=origins, event=self.event) if len(result) > 0: # Update the VCards! PGPKeysImportAsVCards(self.session, arg=[k['fingerprint'] for k in result] ).run() # Previous crypto evaluations may now be out of date, so we # clear the cache so users can see results right away. ClearParseCache(pgpmime=True) return self._success(_n('Imported %d encryption key', 'Imported %d encryption keys', len(result)) % len(result), result=result)
def command(self): args = list(self.args) if '--' in args: spoint = args.index('--') origins = (' '.join(args[(spoint + 1):])).split(', ') args = args[:spoint] else: origins = self.data.get('origins') if len(args) > 1: allowremote = args.pop() else: allowremote = self.data.get('allowremote', ['Y'])[0] if allowremote.lower()[:1] in ('n', 'f'): allowremote = False if '*' in (origins or []): origins = [h.NAME for h in KEY_LOOKUP_HANDLERS] email = " ".join(self.data.get('email', [])) address = " ".join(self.data.get('address', args)) result = dict((k.summary(), k) for k in lookup_crypto_keys(self.session, email or address, strict_email_match=email, event=self.event, allowremote=allowremote, origins=origins)) return self._success( _n('Found %d encryption key', 'Found %d encryption keys', len(result)) % len(result), result=result)
def as_text(self): if not self.result: return "Failed" if not self.result["msg_ids"]: return "Nothing happened" what = [] if self.result["tagged"]: what.append("Tagged " + ", ".join([k["name"] for k, ids in self.result["tagged"]])) if self.result["untagged"]: what.append("Untagged " + ", ".join([k["name"] for k, ids in self.result["untagged"]])) count = len(self.result["msg_ids"]) whats = ", ".join(what) convs = ( _n("%d conversation", "%d conversation", count) if self.result.get("conversations") else _n("%d message", "%d messages", count) ) % count return "%s (%s)" % (whats, convs)
def as_text(self): if not self.result: return 'Failed' if not self.result['msg_ids']: return 'Nothing happened' what = [] if self.result['tagged']: what.append( 'Tagged ' + ', '.join([k['name'] for k, ids in self.result['tagged']])) if self.result['untagged']: what.append('Untagged ' + ', '.join( [k['name'] for k, ids in self.result['untagged']])) count = len(self.result['msg_ids']) whats = ', '.join(what) convs = (_n('%d conversation', '%d conversation', count) if self.result.get('conversations') else _n( '%d message', '%d messages', count)) % count return '%s (%s)' % (whats, convs)
def as_text(self): if not self.result: return 'Failed' if not self.result['msg_ids']: return 'Nothing happened' what = [] if self.result['tagged']: what.append('Tagged ' + ', '.join([k['name'] for k, ids in self.result['tagged']])) if self.result['untagged']: what.append('Untagged ' + ', '.join([k['name'] for k, ids in self.result['untagged']])) count = len(self.result['msg_ids']) whats = ', '.join(what) convs = (_n('%d conversation', '%d conversation', count) if self.result.get('conversations') else _n('%d message', '%d messages', count)) % count return '%s (%s)' % (whats, convs)
def command(self): if len(self.args) > 1: allowremote = self.args.pop() else: allowremote = self.data.get('allowremote', True) address = " ".join(self.data.get('address', self.args)) result = lookup_crypto_keys(self.session, address, event=self.event, allowremote=allowremote) return self._success(_n('Found %d key', 'Found %d keys', len(result) ) % len(result), result=result)
def command(self): if len(self.args) > 1: allowremote = self.args.pop() else: allowremote = self.data.get('allowremote', True) address = " ".join(self.data.get('address', self.args)) result = lookup_crypto_keys(self.session, address, event=self.event, allowremote=allowremote) return self._success(_n('Found %d encryption key', 'Found %d encryption keys', len(result)) % len(result), result=result)
def command(self): if len(self.args) > 1: allowremote = self.args.pop() else: allowremote = self.data.get('allowremote', True) email = " ".join(self.data.get('email', [])) address = " ".join(self.data.get('address', self.args)) result = lookup_crypto_keys(self.session, email or address, strict_email_match=email, event=self.event, allowremote=allowremote) return self._success(_n('Found %d encryption key', 'Found %d encryption keys', len(result)) % len(result), result=result)
def command(self): args = list(self.args) if args: address, fprints, origins = args[0], args[1].split(','), args[2:] else: address = self.data.get('address', [''])[0] fprints = self.data.get('fingerprints', []) origins = self.data.get('origins', []) assert(address or fprints or origins) result = lookup_crypto_keys(self.session, address, get=[f.strip() for f in fprints], origins=origins, event=self.event) return self._success(_n('Imported %d key', 'Imported %d keys', len(result)) % len(result), result=result)
def command(self): args = list(self.args) if args: address, fprints, origins = args[0], args[1].split(','), args[2:] else: address = self.data.get('address', [''])[0] fprints = self.data.get('fingerprints', []) origins = self.data.get('origins', []) assert (address or fprints or origins) result = lookup_crypto_keys(self.session, address, get=[f.strip() for f in fprints], origins=origins, event=self.event) return self._success( _n('Imported %d key', 'Imported %d keys', len(result)) % len(result), result=result)
def command(self): args = list(self.args) if args: pin_key = False address, fprints, origins = args[0], args[1].split(','), args[2:] else: pin_key = self.data.get('pinned', [''])[0].lower()[:1] in ('y', 't') address = self.data.get('address', [''])[0] fprints = self.data.get('fingerprints', []) origins = self.data.get('origins', []) safe_assert(address or fprints or origins) result = lookup_crypto_keys(self.session, address, get=[f.strip() for f in fprints], pin_key=pin_key, vcard=self._get_or_create_vcard(address), origins=origins, event=self.event) if len(result) > 0: # Update the VCards! PGPKeysImportAsVCards(self.session, arg=[k['fingerprint'] for k in result]).run() # The key was looked up based on the given address, so it must have # a user id containing that address, so when it is imported to # VCards, the VCard for that address will list the key. for k in result: k.in_vcards = True # Previous crypto evaluations may now be out of date, so we # clear the cache so users can see results right away. ClearParseCache(pgpmime=True) return self._success( _n('Imported %d encryption key', 'Imported %d encryption keys', len(result)) % len(result), result=result)
def command(self): if len(self.args) > 1: allowremote = self.args.pop() else: allowremote = self.data.get('allowremote', ['Y'])[0] if allowremote.lower()[:1] in ('n', 'f'): allowremote = False origins = self.data.get('origins') if '*' in (origins or []): origins = [h.NAME for h in KEY_LOOKUP_HANDLERS] email = " ".join(self.data.get('email', [])) address = " ".join(self.data.get('address', self.args)) result = lookup_crypto_keys(self.session, email or address, strict_email_match=email, event=self.event, allowremote=allowremote, origins=origins) return self._success(_n('Found %d encryption key', 'Found %d encryption keys', len(result)) % len(result), result=result)
def SendMail(session, msg_mid, from_to_msg_ev_tuples): routes = _RouteTuples(session, from_to_msg_ev_tuples) # Randomize order of routes, so we don't always try the broken # one first. Any failure will bail out, but we do keep track of # our successes via. the event, so eventually everything sendable # should get sent. routes.sort(key=lambda k: random.randint(0, 10)) # Update initial event state before we go through and start # trying to deliver stuff. for frm, sendmail, to, msg, events in routes: for ev in (events or []): for rcpt in to: ev.private_data['>'.join([frm, rcpt])] = False for frm, sendmail, to, msg, events in routes: for ev in events: ev.data['recipients'] = len(ev.private_data.keys()) ev.data['delivered'] = len( [k for k in ev.private_data if ev.private_data[k]]) def mark(msg, events, log=True): for ev in events: ev.flags = Event.RUNNING ev.message = msg if log: session.config.event_log.log_event(ev) session.ui.mark(msg) def fail(msg, events): mark(msg, events, log=True) for ev in events: ev.data['last_error'] = msg raise SendMailError(msg) def smtp_do_or_die(msg, events, method, *args, **kwargs): rc, msg = method(*args, **kwargs) if rc != 250: fail(msg + ' (%s %s)' % (rc, msg), events) # Do the actual delivering... for frm, sendmail, to, msg, events in routes: frm_vcard = session.config.vcards.get_vcard(frm) if 'sendmail' in session.config.sys.debug: sys.stderr.write( _('SendMail: from %s (%s), to %s via %s\n') % (frm, frm_vcard and frm_vcard.random_uid or '', to, sendmail.split('@')[-1])) sm_write = sm_close = lambda: True mark(_('Connecting to %s') % sendmail.split('@')[-1], events) if sendmail.startswith('|'): sendmail %= {"rcpt": ",".join(to)} cmd = sendmail[1:].strip().split() proc = Popen(cmd, stdin=PIPE, long_running=True) sm_startup = None sm_write = proc.stdin.write def sm_close(): proc.stdin.close() rv = proc.wait() if rv != 0: fail(_('%s failed with exit code %d') % (cmd, rv), events) sm_cleanup = lambda: [proc.stdin.close(), proc.wait()] # FIXME: Update session UI with progress info for ev in events: ev.data['proto'] = 'subprocess' ev.data['command'] = cmd[0] elif (sendmail.startswith('smtp:') or sendmail.startswith('smtorp:') or sendmail.startswith('smtpssl:') or sendmail.startswith('smtptls:')): proto = sendmail.split(':', 1)[0] host, port = sendmail.split(':', 1)[1].replace('/', '').rsplit(':', 1) smtp_ssl = proto in ('smtpssl', ) # FIXME: 'smtorp' if '@' in host: userpass, host = host.rsplit('@', 1) user, pwd = userpass.split(':', 1) else: user = pwd = None for ev in events: ev.data['proto'] = proto ev.data['host'] = host ev.data['auth'] = bool(user and pwd) if 'sendmail' in session.config.sys.debug: sys.stderr.write( _('SMTP connection to: %s:%s as %s\n') % (host, port, user or '(anon)')) server = (smtp_ssl and SMTP_SSL or SMTP)(local_hostname='mailpile.local', timeout=25) 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 sm_write(data): for line in data.splitlines(True): if line.startswith('.'): server.send('.') server.send(line) def sm_close(): server.send('\r\n.\r\n') smtp_do_or_die(_('Error spooling mail'), events, server.getreply) def sm_cleanup(): if hasattr(server, 'sock'): server.close() else: fail(_('Invalid sendmail command/SMTP server: %s') % sendmail) try: # Run the entire connect/login sequence in a single timer... if sm_startup: RunTimed(30, sm_startup) mark(_('Preparing message...'), events) msg_string = MessageAsString(CleanMessage(session.config, msg)) total = len(msg_string) while msg_string: if mailpile.util.QUITTING: raise TimedOut(_('Quitting')) mark(('Sending message... (%d%%)') % (100 * (total - len(msg_string)) / total), events, log=False) RunTimed(20, sm_write, msg_string[:20480]) msg_string = msg_string[20480:] RunTimed(10, sm_close) mark( _n('Message sent, %d byte', 'Message sent, %d bytes', total) % total, events) for ev in events: for rcpt in to: vcard = session.config.vcards.get_vcard(rcpt) if vcard: vcard.record_history('send', time.time(), msg_mid) if frm_vcard: vcard.prefer_sender(rcpt, frm_vcard) vcard.save() ev.private_data['>'.join([frm, rcpt])] = True ev.data['bytes'] = total ev.data['delivered'] = len( [k for k in ev.private_data if ev.private_data[k]]) finally: sm_cleanup()
def _do_tagging(self, ops, msg_ids, conversations, save=True, auto=False): idx = self._idx() rv = { 'conversations': False, 'msg_ids': [b36(i) for i in msg_ids], 'tagged': [], 'untagged': [] } for op in ops: tag = self.session.config.get_tag(op[1:]) if tag: # FIXME: This should depend on more factors! # - Tags should have metadata about default scope if conversations is None: conversation = ('flat' not in (self.session.order or '')) if (tag.flag_msg_only or tag.flag_editable or tag.type == 'attribute'): conversation = False else: conversation = conversations if conversation: rv['conversations'] = True tag_id = tag._key tag = tag.copy() tag["tid"] = tag_id if op[0] == '-': removed = idx.remove_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv['untagged'].append( (tag, sorted([b36(i) for i in removed]))) else: added = idx.add_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv['tagged'].append((tag, sorted([b36(i) for i in added]))) # Record behavior if len(msg_ids) < 15: for t in self.session.config.get_tags(type='tagged', default=[]): idx.add_tag(self.session, t._key, msg_idxs=msg_ids) else: self.session.ui.warning('Unknown tag: %s' % op) if rv['conversations']: undo_msg = _n('Untag %d conversation', 'Untag %d conversations', len(msg_ids)) % len(msg_ids) done_msg = _n('Tagged %d conversation', 'Tagged %d conversations', len(msg_ids)) % len(msg_ids) else: undo_msg = _n('Untag %d message', 'Untag %d messages', len(msg_ids)) % len(msg_ids) done_msg = _n('Tagged %d message', 'Tagged %d messages', len(msg_ids)) % len(msg_ids) self.event.data['undo'] = undo_msg self.event.private_data['undo'] = { 'tagged': [[t['tid'], mids] for t, mids in rv['tagged']], 'untagged': [[t['tid'], mids] for t, mids in rv['untagged']], } self.finish(save=save) return self._success(done_msg, rv)
def SendMail(session, msg_mid, from_to_msg_ev_tuples, test_only=False, test_route=None): routes = _RouteTuples(session, from_to_msg_ev_tuples, test_route=test_route) # Randomize order of routes, so we don't always try the broken # one first. Any failure will bail out, but we do keep track of # our successes via. the event, so eventually everything sendable # should get sent. routes.sort(key=lambda k: random.randint(0, 10)) # Update initial event state before we go through and start # trying to deliver stuff. for frm, route, to, msg, events in routes: for ev in (events or []): for rcpt in to: ev.private_data['>'.join([frm, rcpt])] = False for frm, route, to, msg, events in routes: for ev in events: ev.data['recipients'] = len(ev.private_data.keys()) ev.data['delivered'] = len([k for k in ev.private_data if ev.private_data[k]]) def mark(msg, events, log=True): for ev in events: ev.flags = Event.RUNNING ev.message = msg if log: session.config.event_log.log_event(ev) session.ui.mark(msg) def fail(msg, events, details=None): mark(msg, events, log=True) for ev in events: ev.data['last_error'] = msg raise SendMailError(msg, details=details) def smtp_do_or_die(msg, events, method, *args, **kwargs): rc, msg = method(*args, **kwargs) if rc != 250: fail(msg + ' (%s %s)' % (rc, msg), events, details={'smtp_error': '%s: %s' % (rc, msg)}) # Do the actual delivering... for frm, route, to, msg, events in routes: route_description = route['command'] or route['host'] frm_vcard = session.config.vcards.get_vcard(frm) update_to_vcards = msg and msg["x-mp-internal-pubkeys-attached"] if 'sendmail' in session.config.sys.debug: sys.stderr.write(_('SendMail: from %s (%s), to %s via %s\n') % (frm, frm_vcard and frm_vcard.random_uid or '', to, route_description)) sm_write = sm_close = lambda: True mark(_('Sending via %s') % route_description, events) if route['command']: # Note: The .strip().split() here converts our cmd into a list, # which should ensure that Popen does not spawn a shell # with potentially exploitable arguments. cmd = (route['command'] % {"rcpt": ",".join(to)}).strip().split() proc = Popen(cmd, stdin=PIPE, long_running=True) sm_startup = None sm_write = proc.stdin.write def sm_close(): proc.stdin.close() rv = proc.wait() if rv != 0: fail(_('%s failed with exit code %d') % (cmd, rv), events, details={'failed_command': cmd, 'exit_code': rv}) sm_cleanup = lambda: [proc.stdin.close(), proc.wait()] # FIXME: Update session UI with progress info for ev in events: ev.data['proto'] = 'subprocess' ev.data['command'] = cmd[0] elif route['protocol'] in ('smtp', 'smtorp', 'smtpssl', 'smtptls'): proto = route['protocol'] host, port = route['host'], route['port'] user, pwd = route['username'], route['password'] smtp_ssl = proto in ('smtpssl', ) # FIXME: 'smtorp' for ev in events: ev.data['proto'] = proto ev.data['host'] = host ev.data['auth'] = bool(user and pwd) if 'sendmail' in session.config.sys.debug: sys.stderr.write(_('SMTP connection to: %s:%s as %s\n' ) % (host, port, user or '(anon)')) serverbox = [None] def sm_connect_server(): server = (smtp_ssl and SMTP_SSL or SMTP )(local_hostname='mailpile.local', timeout=25) if 'sendmail' in session.config.sys.debug: server.set_debuglevel(1) if smtp_ssl or proto in ('smtorp', 'smtptls'): conn_needs = [ConnBroker.OUTGOING_ENCRYPTED] else: conn_needs = [ConnBroker.OUTGOING_SMTP] try: with ConnBroker.context(need=conn_needs) as ctx: server.connect(host, int(port)) server.ehlo_or_helo_if_needed() except (IOError, OSError, smtplib.SMTPServerDisconnected): fail(_('Failed to connect to %s') % host, events, details={'connection_error': True}) return server def sm_startup(): server = sm_connect_server() if not smtp_ssl: # We always try to enable TLS, even if the user just # requested plain-text smtp. But we only throw errors # if the user asked for encryption. try: server.starttls() except: if proto == 'smtptls': raise InsecureSmtpError() else: server = sm_connect_server() serverbox[0] = server if user and pwd: try: server.login(user.encode('utf-8'), pwd.encode('utf-8')) except UnicodeDecodeError: fail(_('Bad character in username or password'), events, details={'authentication_error': True}) except smtplib.SMTPAuthenticationError: fail(_('Invalid username or password'), events, details={'authentication_error': True}) except smtplib.SMTPException: fail(_('Authentication not supported'), events, details={'authentication_error': True}) smtp_do_or_die(_('Sender rejected by SMTP server'), events, server.mail, frm) for rcpt in to: rc, msg = server.rcpt(rcpt) if (rc == SMTORP_HASHCASH_RCODE and msg.startswith(SMTORP_HASHCASH_PREFIX)): rc, msg = server.rcpt(SMTorP_HashCash(rcpt, msg)) if rc != 250: fail(_('Server rejected recipient: %s') % rcpt, events) rcode, rmsg = server.docmd('DATA') if rcode != 354: fail(_('Server rejected DATA: %s %s') % (rcode, rmsg)) def sm_write(data): server = serverbox[0] for line in data.splitlines(True): if line.startswith('.'): server.send('.') server.send(line) def sm_close(): server = serverbox[0] server.send('\r\n.\r\n') smtp_do_or_die(_('Error spooling mail'), events, server.getreply) def sm_cleanup(): server = serverbox[0] if hasattr(server, 'sock'): server.close() else: fail(_('Invalid route: %s') % route, events) try: # Run the entire connect/login sequence in a single timer, but # give it plenty of time in case the network is lame. if sm_startup: RunTimed(300, sm_startup) if test_only: return True mark(_('Preparing message...'), events) msg_string = MessageAsString(CleanMessage(session.config, msg)) total = len(msg_string) while msg_string: if mailpile.util.QUITTING: raise TimedOut(_('Quitting')) mark(('Sending message... (%d%%)' ) % (100 * (total-len(msg_string))/total), events, log=False) RunTimed(120, sm_write, msg_string[:20480]) msg_string = msg_string[20480:] RunTimed(30, sm_close) mark(_n('Message sent, %d byte', 'Message sent, %d bytes', total ) % total, events) for ev in events: for rcpt in to: vcard = session.config.vcards.get_vcard(rcpt) if vcard: vcard.record_history('send', time.time(), msg_mid) if frm_vcard: vcard.prefer_sender(rcpt, frm_vcard) if update_to_vcards: vcard.pgp_key_shared = int(time.time()) vcard.save() ev.private_data['>'.join([frm, rcpt])] = True ev.data['bytes'] = total ev.data['delivered'] = len([k for k in ev.private_data if ev.private_data[k]]) finally: sm_cleanup() return True
def SendMail(session, msg_mid, from_to_msg_ev_tuples): routes = _RouteTuples(session, from_to_msg_ev_tuples) # Randomize order of routes, so we don't always try the broken # one first. Any failure will bail out, but we do keep track of # our successes via. the event, so eventually everything sendable # should get sent. routes.sort(key=lambda k: random.randint(0, 10)) # Update initial event state before we go through and start # trying to deliver stuff. for frm, sendmail, to, msg, events in routes: for ev in (events or []): for rcpt in to: ev.private_data['>'.join([frm, rcpt])] = False for frm, sendmail, to, msg, events in routes: for ev in events: ev.data['recipients'] = len(ev.private_data.keys()) ev.data['delivered'] = len([k for k in ev.private_data if ev.private_data[k]]) def mark(msg, events, log=True): for ev in events: ev.flags = Event.RUNNING ev.message = msg if log: session.config.event_log.log_event(ev) session.ui.mark(msg) def fail(msg, events): mark(msg, events, log=True) for ev in events: ev.data['last_error'] = msg raise SendMailError(msg) def smtp_do_or_die(msg, events, method, *args, **kwargs): rc, msg = method(*args, **kwargs) if rc != 250: fail(msg + ' (%s %s)' % (rc, msg), events) # Do the actual delivering... for frm, sendmail, to, msg, events in routes: frm_vcard = session.config.vcards.get_vcard(frm) if 'sendmail' in session.config.sys.debug: sys.stderr.write(_('SendMail: from %s (%s), to %s via %s\n' ) % (frm, frm_vcard and frm_vcard.random_uid or '', to, sendmail.split('@')[-1])) sm_write = sm_close = lambda: True mark(_('Connecting to %s') % sendmail.split('@')[-1], events) if sendmail.startswith('|'): sendmail %= {"rcpt": ",".join(to)} cmd = sendmail[1:].strip().split() proc = Popen(cmd, stdin=PIPE, long_running=True) sm_startup = None sm_write = proc.stdin.write def sm_close(): proc.stdin.close() rv = proc.wait() if rv != 0: fail(_('%s failed with exit code %d') % (cmd, rv), events) sm_cleanup = lambda: [proc.stdin.close(), proc.wait()] # FIXME: Update session UI with progress info for ev in events: ev.data['proto'] = 'subprocess' ev.data['command'] = cmd[0] elif (sendmail.startswith('smtp:') or sendmail.startswith('smtorp:') or sendmail.startswith('smtpssl:') or sendmail.startswith('smtptls:')): proto = sendmail.split(':', 1)[0] host, port = sendmail.split(':', 1 )[1].replace('/', '').rsplit(':', 1) smtp_ssl = proto in ('smtpssl', ) # FIXME: 'smtorp' if '@' in host: userpass, host = host.rsplit('@', 1) user, pwd = userpass.split(':', 1) else: user = pwd = None for ev in events: ev.data['proto'] = proto ev.data['host'] = host ev.data['auth'] = bool(user and pwd) if 'sendmail' in session.config.sys.debug: sys.stderr.write(_('SMTP connection to: %s:%s as %s\n' ) % (host, port, user or '(anon)')) server = (smtp_ssl and SMTP_SSL or SMTP )(local_hostname='mailpile.local', timeout=25) 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 sm_write(data): for line in data.splitlines(True): if line.startswith('.'): server.send('.') server.send(line) def sm_close(): server.send('\r\n.\r\n') smtp_do_or_die(_('Error spooling mail'), events, server.getreply) def sm_cleanup(): if hasattr(server, 'sock'): server.close() else: fail(_('Invalid sendmail command/SMTP server: %s') % sendmail) try: # Run the entire connect/login sequence in a single timer... if sm_startup: RunTimed(30, sm_startup) mark(_('Preparing message...'), events) msg_string = MessageAsString(CleanMessage(session.config, msg)) total = len(msg_string) while msg_string: if mailpile.util.QUITTING: raise TimedOut(_('Quitting')) mark(('Sending message... (%d%%)' ) % (100 * (total-len(msg_string))/total), events, log=False) RunTimed(20, sm_write, msg_string[:20480]) msg_string = msg_string[20480:] RunTimed(10, sm_close) mark(_n('Message sent, %d byte', 'Message sent, %d bytes', total ) % total, events) for ev in events: for rcpt in to: vcard = session.config.vcards.get_vcard(rcpt) if vcard: vcard.record_history('send', time.time(), msg_mid) if frm_vcard: vcard.prefer_sender(rcpt, frm_vcard) vcard.save() ev.private_data['>'.join([frm, rcpt])] = True ev.data['bytes'] = total ev.data['delivered'] = len([k for k in ev.private_data if ev.private_data[k]]) finally: sm_cleanup()
def SendMail(session, msg_mid, from_to_msg_ev_tuples, test_only=False, test_route=None): routes = _RouteTuples(session, from_to_msg_ev_tuples, test_route=test_route) # Randomize order of routes, so we don't always try the broken # one first. Any failure will bail out, but we do keep track of # our successes via. the event, so eventually everything sendable # should get sent. routes.sort(key=lambda k: random.randint(0, 10)) # Update initial event state before we go through and start # trying to deliver stuff. for frm, route, to, msg, events in routes: for ev in (events or []): for rcpt in to: ev.private_data['>'.join([frm, rcpt])] = False for frm, route, to, msg, events in routes: for ev in events: ev.data['recipients'] = len(ev.private_data.keys()) ev.data['delivered'] = len( [k for k in ev.private_data if ev.private_data[k]]) def mark(msg, events, log=True): for ev in events: ev.flags = Event.RUNNING ev.message = msg if log: session.config.event_log.log_event(ev) session.ui.mark(msg) def fail(msg, events, details=None): mark(msg, events, log=True) for ev in events: ev.data['last_error'] = msg raise SendMailError(msg, details=details) def smtp_do_or_die(msg, events, method, *args, **kwargs): rc, msg = method(*args, **kwargs) if rc != 250: fail(msg + ' (%s %s)' % (rc, msg), events, details={'smtp_error': '%s: %s' % (rc, msg)}) # Do the actual delivering... for frm, route, to, msg, events in routes: route_description = route['command'] or route['host'] frm_vcard = session.config.vcards.get_vcard(frm) update_to_vcards = msg and msg["x-mp-internal-pubkeys-attached"] if 'sendmail' in session.config.sys.debug: sys.stderr.write( _('SendMail: from %s (%s), to %s via %s\n') % (frm, frm_vcard and frm_vcard.random_uid or '', to, route_description)) sm_write = sm_close = lambda: True mark(_('Sending via %s') % route_description, events) if route['command']: # Note: The .strip().split() here converts our cmd into a list, # which should ensure that Popen does not spawn a shell # with potentially exploitable arguments. cmd = (route['command'] % {"rcpt": ",".join(to)}).strip().split() proc = Popen(cmd, stdin=PIPE, long_running=True) sm_startup = None sm_write = proc.stdin.write def sm_close(): proc.stdin.close() rv = proc.wait() if rv != 0: fail(_('%s failed with exit code %d') % (cmd, rv), events, details={ 'failed_command': cmd, 'exit_code': rv }) sm_cleanup = lambda: [proc.stdin.close(), proc.wait()] # FIXME: Update session UI with progress info for ev in events: ev.data['proto'] = 'subprocess' ev.data['command'] = cmd[0] elif route['protocol'] in ('smtp', 'smtorp', 'smtpssl', 'smtptls'): proto = route['protocol'] host, port = route['host'], route['port'] user, pwd = route['username'], route['password'] smtp_ssl = proto in ('smtpssl', ) # FIXME: 'smtorp' for ev in events: ev.data['proto'] = proto ev.data['host'] = host ev.data['auth'] = bool(user and pwd) if 'sendmail' in session.config.sys.debug: sys.stderr.write( _('SMTP connection to: %s:%s as %s\n') % (host, port, user or '(anon)')) serverbox = [None] def sm_connect_server(): server = (smtp_ssl and SMTP_SSL or SMTP)(local_hostname='mailpile.local', timeout=25) if 'sendmail' in session.config.sys.debug: server.set_debuglevel(1) if smtp_ssl or proto in ('smtorp', 'smtptls'): conn_needs = [ConnBroker.OUTGOING_ENCRYPTED] else: conn_needs = [ConnBroker.OUTGOING_SMTP] try: with ConnBroker.context(need=conn_needs) as ctx: server.connect(host, int(port)) server.ehlo_or_helo_if_needed() except (IOError, OSError, smtplib.SMTPServerDisconnected): fail(_('Failed to connect to %s') % host, events, details={'connection_error': True}) return server def sm_startup(): server = sm_connect_server() if not smtp_ssl: # We always try to enable TLS, even if the user just # requested plain-text smtp. But we only throw errors # if the user asked for encryption. try: server.starttls() server.ehlo_or_helo_if_needed() except: if proto == 'smtptls': raise InsecureSmtpError() else: server = sm_connect_server() serverbox[0] = server if user and pwd: try: server.login(user.encode('utf-8'), pwd.encode('utf-8')) except UnicodeDecodeError: fail(_('Bad character in username or password'), events, details={'authentication_error': True}) except smtplib.SMTPAuthenticationError: fail(_('Invalid username or password'), events, details={'authentication_error': True}) except smtplib.SMTPException: fail(_('Authentication not supported'), events, details={'authentication_error': True}) smtp_do_or_die(_('Sender rejected by SMTP server'), events, server.mail, frm) for rcpt in to: rc, msg = server.rcpt(rcpt) if (rc == SMTORP_HASHCASH_RCODE and msg.startswith(SMTORP_HASHCASH_PREFIX)): rc, msg = server.rcpt(SMTorP_HashCash(rcpt, msg)) if rc != 250: fail(_('Server rejected recipient: %s') % rcpt, events) rcode, rmsg = server.docmd('DATA') if rcode != 354: fail(_('Server rejected DATA: %s %s') % (rcode, rmsg)) def sm_write(data): server = serverbox[0] for line in data.splitlines(True): if line.startswith('.'): server.send('.') server.send(line) def sm_close(): server = serverbox[0] server.send('\r\n.\r\n') smtp_do_or_die(_('Error spooling mail'), events, server.getreply) def sm_cleanup(): server = serverbox[0] if hasattr(server, 'sock'): server.close() else: fail(_('Invalid route: %s') % route, events) try: # Run the entire connect/login sequence in a single timer, but # give it plenty of time in case the network is lame. if sm_startup: RunTimed(300, sm_startup) if test_only: return True mark(_('Preparing message...'), events) msg_string = MessageAsString(CleanMessage(session.config, msg)) total = len(msg_string) while msg_string: if mailpile.util.QUITTING: raise TimedOut(_('Quitting')) mark(('Sending message... (%d%%)') % (100 * (total - len(msg_string)) / total), events, log=False) RunTimed(120, sm_write, msg_string[:20480]) msg_string = msg_string[20480:] RunTimed(30, sm_close) mark( _n('Message sent, %d byte', 'Message sent, %d bytes', total) % total, events) for ev in events: for rcpt in to: vcard = session.config.vcards.get_vcard(rcpt) if vcard: vcard.record_history('send', time.time(), msg_mid) if frm_vcard: vcard.prefer_sender(rcpt, frm_vcard) if update_to_vcards: vcard.pgp_key_shared = int(time.time()) vcard.save() ev.private_data['>'.join([frm, rcpt])] = True ev.data['bytes'] = total ev.data['delivered'] = len( [k for k in ev.private_data if ev.private_data[k]]) finally: sm_cleanup() return True
def _do_tagging(self, ops, msg_ids, conversations, save=True, auto=False): idx = self._idx() rv = { 'conversations': False, 'msg_ids': [b36(i) for i in msg_ids], 'tagged': [], 'untagged': [] } for op in ops: tag = self.session.config.get_tag(op[1:]) if tag: # FIXME: This should depend on more factors! # - Tags should have metadata about default scope if conversations is None: conversation = ('flat' not in (self.session.order or '')) if (tag.flag_msg_only or tag.flag_editable or tag.type == 'attribute'): conversation = False else: conversation = conversations if conversation: rv['conversations'] = True tag_id = tag._key tag = tag.copy() tag["tid"] = tag_id if op[0] == '-': removed = idx.remove_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv['untagged'].append((tag, sorted([b36(i) for i in removed]))) else: added = idx.add_tag(self.session, tag_id, msg_idxs=msg_ids, conversation=conversation) rv['tagged'].append((tag, sorted([b36(i) for i in added]))) # Record behavior if len(msg_ids) < 15: for t in self.session.config.get_tags(type='tagged', default=[]): idx.add_tag(self.session, t._key, msg_idxs=msg_ids) else: self.session.ui.warning('Unknown tag: %s' % op) if rv['conversations']: undo_msg = _n('Untag %d conversation', 'Untag %d conversations', len(msg_ids)) % len(msg_ids) done_msg = _n('Tagged %d conversation', 'Tagged %d conversations', len(msg_ids)) % len(msg_ids) else: undo_msg = _n('Untag %d message', 'Untag %d messages', len(msg_ids)) % len(msg_ids) done_msg = _n('Tagged %d message', 'Tagged %d messages', len(msg_ids)) % len(msg_ids) self.event.data['undo'] = undo_msg self.event.private_data['undo'] = { 'tagged': [[t['tid'], mids] for t, mids in rv['tagged']], 'untagged': [[t['tid'], mids] for t, mids in rv['untagged']], } self.finish(save=save) return self._success(done_msg, rv)