def TestPop3Settings(session, settings, event): password = IndirectPassword(session.config, settings['password']) conn = _open_pop3_mailbox(session, event, settings['host'], int(settings['port']), settings['username'], password, settings.get('auth_type', 'password'), settings['protocol'], True) if conn: conn.close() return True return False
def open_mailbox(self, mbx_id, mfn): my_cfg = self.my_config if 'src:' in mfn[:5] and FormatMbxId(mbx_id) in my_cfg.mailbox: debug = ('pop3' in self.session.config.sys.debug) and 99 or 0 password = IndirectPassword(self.session.config, my_cfg.password) return _open_pop3_mailbox(self.event, my_cfg.host, my_cfg.port, my_cfg.username, password, my_cfg.auth_type, my_cfg.protocol, debug, throw=POP3_IOError) return None
def _connect_imap(session, settings, event, conn_cls=None, timeout=30, throw=False, logged_in_cb=None, source=None): def timed(*args, **kwargs): if source is not None: kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key, ) return RunTimed(timeout, *args, **kwargs) def timed_imap(*args, **kwargs): if source is not None: kwargs['unique_thread'] = 'imap/%s' % (source.my_config._key, ) return _parse_imap(RunTimed(timeout, *args, **kwargs)) conn = None try: # 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')] } # If we are given a conn class, use that - this allows mocks for # testing. if not conn_cls: req_stls = (settings.get('protocol') == 'imap_tls') want_ssl = (settings.get('protocol') == 'imap_ssl') conn_cls = IMAP4_SSL if want_ssl else IMAP4 else: req_stls = want_ssl = False def mkconn(): if want_ssl: need = [ConnBroker.OUTGOING_IMAPS] else: need = [ConnBroker.OUTGOING_IMAP] with ConnBroker.context(need=need): return conn_cls(settings.get('host'), int(settings.get('port'))) conn = timed(mkconn) if hasattr(conn, 'sock'): conn.sock.settimeout(120) conn.debug = ('imaplib' in session.config.sys.debug) and 4 or 0 ok, data = timed_imap(conn.capability) if ok: capabilities = set(' '.join(data).upper().split()) else: capabilities = set() if req_stls or ('STARTTLS' in capabilities and not want_ssl): try: ok, data = timed_imap(conn.starttls) if ok: # Fetch capabilities again after STARTTLS ok, data = timed_imap(conn.capability) capabilities = set(' '.join(data).upper().split()) # Update the protocol to avoid getting downgraded later if settings.get('protocol', '') != 'imap_ssl': settings['protocol'] = 'imap_tls' except (IMAP4.error, IOError, socket.error): ok = False if not ok: ev['error'] = [ 'tls', _('Failed to make a secure TLS connection'), '%s:%s' % (settings.get('host'), settings.get('port')) ] if throw: raise throw(ev['error'][1]) return WithaBool(False) username = password = "" try: error_type = 'auth' error_msg = _('Invalid username or password') username = settings.get('username', '').encode('utf-8') password = IndirectPassword(session.config, settings.get('password', '')).encode('utf-8') if (settings.get('auth_type', '').lower() == 'oauth2' and 'AUTH=XOAUTH2' in capabilities): error_type = 'oauth2' error_msg = _('Access denied by mail server') token_info = OAuth2.GetFreshTokenInfo(session, username) if not (username and token_info and token_info.access_token): raise ValueError("Missing configuration") ok, data = timed_imap( conn.authenticate, 'XOAUTH2', lambda challenge: OAuth2.XOAuth2Response( username, token_info)) if not ok: token_info.access_token = '' else: ok, data = timed_imap(conn.login, username, password) except (IMAP4.error, UnicodeDecodeError, ValueError): ok, data = False, None if not ok: auth_summary = '' if source is not None: auth_summary = source._summarize_auth() ev['error'] = [error_type, error_msg, username, auth_summary] if throw: raise throw(ev['error'][1]) return WithaBool(False) if logged_in_cb is not None: logged_in_cb(conn, ev, capabilities) return conn except TimedOut: if 'imap' in session.config.sys.debug: session.ui.debug('%s' % traceback.format_exc()) ev['error'] = ['timeout', _('Connection timed out')] except (ssl.CertificateError, ssl.SSLError): if 'imap' in session.config.sys.debug: session.ui.debug('%s' % traceback.format_exc()) ev['error'] = [ 'tls', _('Failed to make a secure TLS connection'), '%s:%s' % (settings.get('host'), settings.get('port')) ] except (IMAP_IOError, IMAP4.error): if 'imap' in session.config.sys.debug: session.ui.debug('%s' % traceback.format_exc()) ev['error'] = ['protocol', _('An IMAP protocol error occurred')] except (IOError, AttributeError, socket.error): if 'imap' in session.config.sys.debug: session.ui.debug('%s' % 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 None
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, clear_errors=False): for ev in events: ev.flags = Event.RUNNING ev.message = msg if clear_errors: if 'last_error' in ev.data: del ev.data['last_error'] if 'last_error_details' in ev.data: del ev.data['last_error_details'] if log: session.config.event_log.log_event(ev) session.ui.mark(msg) def fail(msg, events, details=None, exception=SendMailError): mark(msg, events, log=True) for ev in events: ev.data['last_error'] = msg if details: ev.data['last_error_details'] = details raise exception(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, clear_errors=True) 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 = route['username'] pwd = IndirectPassword(session.config, route['password']) auth_type = route['auth_type'] or '' 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(): try: 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 else: server = sm_connect_server() except (ssl.CertificateError, ssl.SSLError): fail(_('Failed to make a secure TLS connection'), events, details={ 'tls_error': True, 'server': '%s:%d' % (host, port)}, exception=InsecureSmtpError) serverbox[0] = server if user: try: if auth_type.lower() == 'oauth2': from mailpile.plugins.oauth import OAuth2 tok_info = OAuth2.GetFreshTokenInfo(session, user) if not (user and tok_info and tok_info.access_token): fail(_('Access denied by mail server'), events, details={'oauth_error': True, 'username': user}) authstr = (OAuth2.XOAuth2Response(user, tok_info) ).encode('base64').replace('\n', '') server.docmd('AUTH', 'XOAUTH2 ' + authstr) else: server.login(user.encode('utf-8'), (pwd or '').encode('utf-8')) except UnicodeDecodeError: fail(_('Bad character in username or password'), events, details={'authentication_error': True, 'username': user}) except smtplib.SMTPAuthenticationError: fail(_('Invalid username or password'), events, details={'authentication_error': True, 'username': user}) except smtplib.SMTPException: # If the server does not support authentication, assume # it's passwordless and try to carry one anyway. pass smtp_do_or_die(_('Sender rejected by SMTP server'), events, server.mail, frm) for rcpt in to: rc, msg = server.rcpt(rcpt) if (rc == SMTORP_HASHCASH_RCODE and msg.startswith(SMTORP_HASHCASH_PREFIX)): rc, msg = server.rcpt(SMTorP_HashCash(rcpt, msg)) if rc != 250: fail(_('Server rejected recipient: %s') % rcpt, events) rcode, rmsg = server.docmd('DATA') if rcode != 354: fail(_('Server rejected DATA: %s %s') % (rcode, rmsg)) def sm_write(data): server = serverbox[0] for line in data.splitlines(True): if line.startswith('.'): server.send('.') server.send(line) def sm_close(): server = serverbox[0] server.send('\r\n.\r\n') smtp_do_or_die(_('Error spooling mail'), events, server.getreply) def sm_cleanup(): server = serverbox[0] if hasattr(server, 'sock'): server.close() else: fail(_('Invalid route: %s') % route, events) try: # Run the entire connect/login sequence in a single timer, but # give it plenty of time in case the network is lame. if sm_startup: RunTimed(300, sm_startup) if test_only: return True mark(_('Preparing message…'), events) msg_string = MessageAsString(CleanMessage(session.config, msg)) total = len(msg_string) while msg_string: if mailpile.util.QUITTING: raise TimedOut(_('Quitting')) mark(('Sending message... (%d%%)' ) % (100 * (total-len(msg_string))/total), events, log=False) RunTimed(120, sm_write, msg_string[:20480]) msg_string = msg_string[20480:] RunTimed(30, sm_close) mark(_n('Message sent, %d byte', 'Message sent, %d bytes', total ) % total, events) for ev in events: for rcpt in to: vcard = session.config.vcards.get_vcard(rcpt) if vcard: vcard.record_history('send', time.time(), msg_mid) if frm_vcard: vcard.prefer_sender(rcpt, frm_vcard) if update_to_vcards: vcard.pgp_key_shared = int(time.time()) vcard.save() ev.private_data['>'.join([frm, rcpt])] = True ev.data['bytes'] = total ev.data['delivered'] = len([k for k in ev.private_data if ev.private_data[k]]) finally: sm_cleanup() return True