def chat(self, gpg_args, callback, *args, **kwargs): """This lets a callback have a chat with the GPG process...""" gpg_args = [self.gpgbinary, "--utf8-strings", "--no-use-agent", "--no-tty", "--command-fd=0", "--status-fd=1"] + (gpg_args or []) if self.homedir: gpg_args.insert(1, "--homedir=%s" % self.homedir) proc = None try: # Here we go! proc = Popen(gpg_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0) return callback(proc, *args, **kwargs) finally: # Close this so GPG will terminate. This should already have # been done, but we're handling errors here... if proc and proc.stdin: proc.stdin.close() if proc: proc.wait()
def _popen(self, command, fd, long_running): self._reading = True proc = Popen(command, stdin=fd, stdout=PIPE, stderr=PIPE, bufsize=0, long_running=long_running) return proc, proc.stdout
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 run(self, args=None, gpg_input=None, outputfd=None, partial_read_ok=False, send_passphrase=False, _raise=None): self.outputbuffers = dict([(x, []) for x in self.outputfds]) self.threads = {} wtf = ' '.join(args) args = args[:] if args else [] args.insert(0, self.gpgbinary) args.insert(1, "--utf8-strings") args.insert(1, "--with-colons") args.insert(1, "--verbose") args.insert(1, "--batch") args.insert(1, "--enable-progress-filter") if not self.use_agent: args.insert(1, "--no-use-agent") if self.homedir: args.insert(1, "--homedir=%s" % self.homedir) gpg_retcode = -1 proc = None try: args.insert(1, "--status-fd=2") if self.passphrase and send_passphrase: if self.use_agent: args.insert(1, "--no-use-agent") args.insert(2, "--passphrase-fd=0") if not self.passphrase and send_passphrase: self.debug('Running WITHOUT PASSPHRASE %s' % ' '.join(args)) self.debug(traceback.format_stack()) else: self.debug('Running %s' % ' '.join(args)) # Here we go! proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0) # GnuPG is a bit crazy, and requires that the passphrase # be sent and the filehandle closed before anything else # interesting happens. if self.passphrase and send_passphrase: c = self.passphrase.read(BLOCKSIZE) while c != '': proc.stdin.write(c) c = self.passphrase.read(BLOCKSIZE) proc.stdin.write('\n') self.threads = { "stderr": StreamReader('gpgi-stderr(%s)' % wtf, proc.stderr, self.parse_stderr) } if outputfd: self.threads["stdout"] = StreamReader( 'gpgi-stdout-to-fd(%s)' % wtf, proc.stdout, outputfd.write, lines=False) else: self.threads["stdout"] = StreamReader( 'gpgi-stdout-parsed(%s)' % wtf, proc.stdout, self.parse_stdout) if gpg_input: # If we have output, we just stream it. Technically, this # doesn't really need to be a thread at the moment. self.debug('<<STDOUT<< %s' % gpg_input) StreamWriter('gpgi-output(%s)' % wtf, proc.stdin, gpg_input, partial_write_ok=partial_read_ok).join() else: proc.stdin.close() # Reap GnuPG gpg_retcode = proc.wait() finally: # Close this so GPG will terminate. This should already have # been done, but we're handling errors here... if proc and proc.stdin: proc.stdin.close() # Reap the threads self._reap_threads() if outputfd: outputfd.close() if gpg_retcode != 0 and _raise: raise _raise('GnuPG failed, exit code: %s' % gpg_retcode) return gpg_retcode, self.outputbuffers
def _real_startup(config): while config.http_worker is None: time.sleep(0.1) try: session_id = config.http_worker.httpd.make_session_id(None) mailpile.auth.SetLoggedIn(None, user='******', session_id=session_id) cookie = config.http_worker.httpd.session_cookie sspec = config.http_worker.httpd.sspec base_url = 'http://%s:%s' % sspec script_dir = os.path.dirname(os.path.realpath(__file__)) script = os.path.join(script_dir, 'gui-o-matic.py') global __GUI__ gui = __GUI__ = Popen( ['python', '-u', script], bufsize=1, # line buffered stdin=PIPE, stderr=PIPE, long_running=True) stderr = [] eater = threading.Thread(target=output_eater, args=[gui.stderr, stderr]) eater.name = 'GUI(stderr)' eater.daemon = True eater.start() ico = lambda s: os.path.join(script_dir, 'icons-%(theme)s', s) gui.stdin.write( json.dumps({ 'app_name': 'Mailpile', 'indicator_icons': { 'startup': ico('startup.png'), 'normal': ico('normal.png'), 'working': ico('working.png'), 'attention': ico('attention.png'), 'shutdown': ico('shutdown.png') }, 'indicator_menu': [{ 'label': _('Starting up ...'), 'item': 'status' }, { 'label': _('Open Mailpile'), 'item': 'open', 'op': 'show_url', 'args': [base_url] }, { 'label': _('Quit'), 'item': 'quit', 'op': 'get_url', 'args': [base_url + '/api/0/quitquitquit/'] }], 'http_cookies': { base_url: [[cookie, session_id]] }, }).strip() + '\nOK GO\n') indicator('set_menu_sensitive', item='quit') indicator('set_menu_sensitive', item='open') # FIXME: This sleep is lame time.sleep(5) if (gui.poll() is not None) or mailpile.util.QUITTING: return except: # If the basic indicator setup fails, we just assume it doesn't # work and go silently dead... return try: # ...however, getting this far means if the indicator dies, then # the user tried to quit the app, so we should cooperate and die # (via the except below). while config.index is None or not config.tags: if mailpile.util.QUITTING: return if gui.poll() is not None: return time.sleep(1) indicator('set_status_normal') # FIXME: We should do more with the indicator... this is a bit lame. while True: if mailpile.util.QUITTING: indicator('set_status_shutdown') indicator('set_menu_sensitive', item='open', sensitive=False) indicator('set_menu_sensitive', item='quit', sensitive=False) indicator('set_menu_label', item='status', label=_('Shutting down...')) time.sleep(300) else: indicator('set_menu_label', item='status', label=_('%d messages') % len(config.index and config.index.INDEX or [])) time.sleep(5) except AttributeError: pass finally: try: if not mailpile.util.QUITTING: Quit(Session(config)).run() except: pass
def run(self, args=[], gpg_input=None, outputfd=None, partial_read_ok=False, _raise=None): self.outputbuffers = dict([(x, []) for x in self.outputfds]) self.pipes = {} self.threads = {} wtf = ' '.join(args) args.insert(0, self.gpgbinary) args.insert(1, "--utf8-strings") args.insert(1, "--with-colons") args.insert(1, "--verbose") args.insert(1, "--batch") args.insert(1, "--enable-progress-filter") if not self.use_agent: args.insert(1, "--no-use-agent") if self.homedir: args.insert(1, "--homedir=%s" % self.homedir) gpg_retcode = -1 proc = status_handle = passphrase_handle = None status_pipe = passphrase_pipe = [None, None] popen_keeps_open = [] try: status_pipe = os.pipe() status_handle = os.fdopen(status_pipe[0], "r") args.insert(1, "--status-fd=%d" % status_pipe[1]) popen_keeps_open.append(status_pipe[1]) if self.passphrase: passphrase_pipe = os.pipe() passphrase_handle = os.fdopen(passphrase_pipe[1], "w") if self.use_agent: args.insert(1, "--no-use-agent") args.insert(2, "--passphrase-fd=%d" % passphrase_pipe[0]) popen_keeps_open.append(passphrase_pipe[0]) # Here we go! proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0, keep_open=popen_keeps_open) # GnuPG is a bit crazy, and requires that the passphrase # be sent and the filehandle closed before anything else # interesting happens. if self.passphrase: c = self.passphrase.read(BLOCKSIZE) while c != '': passphrase_handle.write(c) c = self.passphrase.read(BLOCKSIZE) passphrase_handle.write('\n') passphrase_handle.close() self.threads = { "stderr": StreamReader('gpgi-stderr(%s)' % wtf, proc.stderr, self.parse_stderr), "status": StreamReader('gpgi-status(%s)' % wtf, status_handle, self.parse_status), } if outputfd: self.threads["stdout"] = StreamReader( 'gpgi-stdout-to-fd(%s)' % wtf, proc.stdout, outputfd.write, lines=False) else: self.threads["stdout"] = StreamReader( 'gpgi-stdout-parsed(%s)' % wtf, proc.stdout, self.parse_stdout) if gpg_input: # If we have output, we just stream it. Technically, this # doesn't really need to be a thread at the moment. StreamWriter('gpgi-output(%s)' % wtf, proc.stdin, gpg_input, partial_write_ok=partial_read_ok).join() else: proc.stdin.close() # Reap GnuPG gpg_retcode = proc.wait() finally: def closer(method, *args): try: method(*args) except (IOError, OSError): pass # Here we close GPG's end of the status pipe, because # otherwise things may hang. We also close the passphrase pipe # at both ends, as it should be completely finished. for fdn in (status_pipe[1], passphrase_pipe[0], passphrase_pipe[1]): if fdn is not None: closer(os.close, fdn) for fd in (passphrase_handle,): if fd is not None: closer(fd.close) # Close this so GPG will terminate. This should already have # been done, but we're handling errors here... if proc and proc.stdin: closer(proc.stdin.close) # Reap the threads for name, thr in self.threads.iteritems(): if thr.isAlive(): thr.join(timeout=15) if thr.isAlive(): print 'SCARY WARNING: FAILED TO REAP THREAD %s' % thr if outputfd: outputfd.close() if gpg_retcode != 0 and _raise: raise _raise('GnuPG failed, exit code: %s' % gpg_retcode) return gpg_retcode, self.outputbuffers
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 run(self, args=None, gpg_input=None, outputfd=None, partial_read_ok=False, _raise=None): self.outputbuffers = dict([(x, []) for x in self.outputfds]) self.pipes = {} self.threads = {} wtf = ' '.join(args) args = args[:] if args else [] args.insert(0, self.gpgbinary) args.insert(1, "--utf8-strings") args.insert(1, "--with-colons") args.insert(1, "--verbose") args.insert(1, "--batch") args.insert(1, "--enable-progress-filter") if not self.use_agent: args.insert(1, "--no-use-agent") if self.homedir: args.insert(1, "--homedir=%s" % self.homedir) gpg_retcode = -1 proc = status_pipe = passphrase_pipe = None popen_keeps_open = [] try: status_pipe = Safe_Pipe() args.insert(1, "--status-fd=%d" % status_pipe.write_end.fileno()) popen_keeps_open.append(status_pipe.write_end) if self.passphrase: passphrase_pipe = Safe_Pipe() if self.use_agent: args.insert(1, "--no-use-agent") args.insert( 2, "--passphrase-fd=%d" % passphrase_pipe.read_end.fileno()) popen_keeps_open.append(passphrase_pipe.read_end) # Here we go! proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0, keep_open=popen_keeps_open) # GnuPG is a bit crazy, and requires that the passphrase # be sent and the filehandle closed before anything else # interesting happens. if self.passphrase: c = self.passphrase.read(BLOCKSIZE) while c != '': passphrase_pipe.write(c) c = self.passphrase.read(BLOCKSIZE) passphrase_pipe.write('\n') passphrase_pipe.write_end.close() self.threads = { "stderr": StreamReader('gpgi-stderr(%s)' % wtf, proc.stderr, self.parse_stderr), "status": StreamReader('gpgi-status(%s)' % wtf, status_pipe.read_end, self.parse_status), } if outputfd: self.threads["stdout"] = StreamReader('gpgi-stdout-to-fd(%s)' % wtf, proc.stdout, outputfd.write, lines=False) else: self.threads["stdout"] = StreamReader( 'gpgi-stdout-parsed(%s)' % wtf, proc.stdout, self.parse_stdout) if gpg_input: # If we have output, we just stream it. Technically, this # doesn't really need to be a thread at the moment. StreamWriter('gpgi-output(%s)' % wtf, proc.stdin, gpg_input, partial_write_ok=partial_read_ok).join() else: proc.stdin.close() # Reap GnuPG gpg_retcode = proc.wait() finally: # Here we close GPG's end of the status pipe, because # otherwise things may hang. We also close the passphrase pipe # at both ends, as it should be completely finished. if status_pipe: status_pipe.write_end.close() if passphrase_pipe: passphrase_pipe.close() # Close this so GPG will terminate. This should already have # been done, but we're handling errors here... if proc and proc.stdin: proc.stdin.close() # Reap the threads self._reap_threads() if outputfd: outputfd.close() if gpg_retcode != 0 and _raise: raise _raise('GnuPG failed, exit code: %s' % gpg_retcode) return gpg_retcode, self.outputbuffers
def run(self, args=None, gpg_input=None, outputfd=None, partial_read_ok=False, _raise=None): self.outputbuffers = dict([(x, []) for x in self.outputfds]) self.pipes = {} self.threads = {} wtf = ' '.join(args) args = args[:] if args else [] args.insert(0, self.gpgbinary) args.insert(1, "--utf8-strings") args.insert(1, "--with-colons") args.insert(1, "--verbose") args.insert(1, "--batch") args.insert(1, "--enable-progress-filter") if not self.use_agent: args.insert(1, "--no-use-agent") if self.homedir: args.insert(1, "--homedir=%s" % self.homedir) gpg_retcode = -1 proc = status_pipe = passphrase_pipe = None popen_keeps_open = [] try: status_pipe = Safe_Pipe() args.insert(1, "--status-fd=%d" % status_pipe.write_end.fileno()) popen_keeps_open.append(status_pipe.write_end) if self.passphrase: passphrase_pipe = Safe_Pipe() if self.use_agent: args.insert(1, "--no-use-agent") args.insert(2, "--passphrase-fd=%d" % passphrase_pipe.read_end.fileno()) popen_keeps_open.append(passphrase_pipe.read_end) # Here we go! proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=0, keep_open=popen_keeps_open) # GnuPG is a bit crazy, and requires that the passphrase # be sent and the filehandle closed before anything else # interesting happens. if self.passphrase: c = self.passphrase.read(BLOCKSIZE) while c != '': passphrase_pipe.write(c) c = self.passphrase.read(BLOCKSIZE) passphrase_pipe.write('\n') passphrase_pipe.write_end.close() self.threads = { "stderr": StreamReader('gpgi-stderr(%s)' % wtf, proc.stderr, self.parse_stderr), "status": StreamReader('gpgi-status(%s)' % wtf, status_pipe.read_end, self.parse_status), } if outputfd: self.threads["stdout"] = StreamReader( 'gpgi-stdout-to-fd(%s)' % wtf, proc.stdout, outputfd.write, lines=False) else: self.threads["stdout"] = StreamReader( 'gpgi-stdout-parsed(%s)' % wtf, proc.stdout, self.parse_stdout) if gpg_input: # If we have output, we just stream it. Technically, this # doesn't really need to be a thread at the moment. StreamWriter('gpgi-output(%s)' % wtf, proc.stdin, gpg_input, partial_write_ok=partial_read_ok).join() else: proc.stdin.close() # Reap GnuPG gpg_retcode = proc.wait() finally: # Here we close GPG's end of the status pipe, because # otherwise things may hang. We also close the passphrase pipe # at both ends, as it should be completely finished. if status_pipe: status_pipe.write_end.close() if passphrase_pipe: passphrase_pipe.close() # Close this so GPG will terminate. This should already have # been done, but we're handling errors here... if proc and proc.stdin: proc.stdin.close() # Reap the threads self._reap_threads() if outputfd: outputfd.close() if gpg_retcode != 0 and _raise: raise _raise('GnuPG failed, exit code: %s' % gpg_retcode) return gpg_retcode, self.outputbuffers