def save_contact(contact): logger.debug(f'Saving contact: {contact}') db.session.add(contact) db.session.commit() get_contacts.cache_clear()
def run_server(): logger.debug(f'Starting server on {SERVER_HOST}:{server.get_port()}') try: server.serve() except Exception as e: logger.exception(f'Exception in server thread!: {e}')
def get_capabilities(self): if self.capabilities is None: with self.connection_pool.get_connection() as connection: self.capabilities = connection.capabilities() logger.debug(f'Loaded capabilities for {self.name}: {self.capabilities}') return self.capabilities
def get_connection(self): if not self.password: self.password = get_password('account', self.host, self.username) if not self.password: raise ConnectionSettingsError( self.account, 'Missing SMTP password! Please re-enter your password in settings.', ) server_string = ( f'{self.username}@{self.host}:{self.port} (ssl={self.ssl}, tls={self.tls})' ) logger.debug(f'Connecting to SMTP server: {server_string}') cls = SMTP_SSL if self.ssl else SMTP smtp = cls(self.host, self.port) if DEBUG_SMTP: smtp.set_debuglevel(1) smtp.connect(self.host, self.port) if self.tls: smtp.starttls() smtp.login(self.username, self.password) yield smtp smtp.quit()
def delete_contact(contact): logger.debug(f'Deleting contact: {contact}') db.session.delete(contact) db.session.commit() get_contacts.cache_clear()
def __init__(self, imap_host, *args, **kwargs): logger.debug(f'Creating fake IMAP: ({args}, {kwargs})') self._imap_host = imap_host for folder in ALIAS_FOLDERS + OTHER_FOLDERS: self._ensure_folder(folder)
def save_contacts(*contacts): logger.debug(f'Saving {len(contacts)} contacts') for contact in contacts: db.session.add(contact) db.session.commit() get_contacts.cache_clear()
def __init__(self, name, uid_offset): logger.debug(f'Creating fake folder: {name}') self.name = name self.uids = tuple(range(uid_offset + 1, uid_offset + 10)) self.status = { b'UIDVALIDITY': 1, }
def __init__(self, name): logger.debug(f'Creating fake folder: {name}') self.name = name self.uids = tuple(range(1, 10)) self.status = { b'UIDVALIDITY': choice(self.uids), }
def set_settings(new_settings: dict) -> None: validate_settings(new_settings) logger.debug(f'Writing new settings: {new_settings}') json_data = json.dumps(new_settings, indent=4) with open(SETTINGS_FILE, 'w') as file: file.write(json_data) get_settings.cache_clear()
def set_settings(new_settings: dict) -> None: validate_settings(new_settings) logger.debug(f'Writing new settings: {new_settings}') json_data = json.dumps(new_settings, indent=4) with open(SETTINGS_FILE, 'w') as file: file.write(json_data) # Reset pydash.memoize's cache for next call to `get_settings` get_settings.cache = {}
def ensure_folder_exists(self, folder): folder = self.get_folder(folder) if not folder.exists: logger.debug(f'Creating folder {self.name}/{folder.name}') with self.get_imap_connection() as connection: connection.create_folder(folder.name) folder.get_and_set_email_uids() return folder.name
def create_send(): data = request.get_json() message_data = get_or_400(data, 'message') message_data['reply_all'] = data.get('reply_all', False) message_data['forward'] = data.get('forward', False) uid = str(uuid4()) SEND_WINDOW_DATA[uid] = message_data logger.debug(f'Created send data with UID={uid}') endpoint = f'/send/{uid}' return jsonify(endpoint=endpoint)
def set_window_settings(width: int, height: int, left: int, top: int) -> None: window_settings = { 'width': width, 'height': height, 'x': left, 'y': top, } logger.debug(f'Writing window settings: {window_settings}') with open(WINDOW_CACHE_FILE, 'wb') as f: f.write(pickle.dumps(window_settings))
def main(): logger.info(f'\n#\n# Booting Kanmail {get_version()}\n#') init_window_hacks() boot() server_thread = Thread(name='Server', target=run_server) server_thread.daemon = True server_thread.start() run_thread(validate_or_remove_license) run_thread(run_cache_cleanup_later) # Ensure the webserver is up & running by polling it waits = 0 while waits < 10: try: response = requests.get(f'http://{SERVER_HOST}:{server.get_port()}/ping') response.raise_for_status() except requests.RequestException as e: logger.warning(f'Waiting for main window: {e}') sleep(0.1 * waits) waits += 1 else: break else: logger.critical('Webserver did not start properly!') sys.exit(2) create_window( unique_key='main', **get_window_settings(), ) # Let's hope this thread doesn't fail! monitor_thread = Thread( name='Thread monitor', target=monitor_threads, args=(server_thread,), ) monitor_thread.daemon = True monitor_thread.start() if DEBUG: sleep(1) # give webpack a second to start listening # Start the GUI - this will block until the main window is destroyed webview.start(gui=GUI_LIB, debug=DEBUG) logger.debug('Main window closed, shutting down...') server.stop() sys.exit()
def send_email(self, **send_kwargs): message = make_email_message(**send_kwargs) subject = send_kwargs.get('subject') with self.smtp_connection.get_connection() as smtp: logger.debug( (f'Send email via SMTP/{self.smtp_connection}: ' f'{subject}, from {message["From"]} => {message["To"]}')) smtp.send_message(message) if self.settings['folders'].get('save_sent_copies'): sent_folder = self.get_folder('sent') sent_folder.append_email_message(message)
def boot(prepare_server: bool = True) -> None: if prepare_server: server.prepare() logger.debug(f'App client root is: {CLIENT_ROOT}') logger.debug(f'App session token is: {SESSION_TOKEN}') logger.debug(f'App server port: http://{SERVER_HOST}:{server.get_port()}') if environ.get('KANMAIL_FAKE_IMAP') == 'on': logger.debug('Using fixtures, faking the IMAP client & responses!') from kanmail.server.mail.connection_mocks import bootstrap_fake_connections bootstrap_fake_connections() from kanmail import secrets # noqa: F401 from kanmail.server.views import error # noqa: F401 # API views from kanmail.server.views import ( # noqa: F401 accounts_api, contacts_api, email_api, license_api, oauth_api, settings_api, update_api, window_api, ) # Database models from kanmail.server.mail.contacts import Contact # noqa: F401 from kanmail.server.mail.allowed_images import AllowedImage # noqa: F401 db.create_all()
def validate_or_remove_license() -> None: ''' A "hard" check for the presence and validity of a license key. Will also remove any invalid license file/keyring item. This function is executed in a separate thread on app start. ''' license_email = check_get_license_email() if not license_email: return logger.debug(f'Validating license for {license_email}...') combined_token = get_password('license', LICENSE_SERVER_APP_TOKEN, license_email) if not combined_token: return token, device_token = combined_token.split(':') try: response = requests.get( f'{LICENSE_SERVER_URL}/api/check', json={ 'app_token': LICENSE_SERVER_APP_TOKEN, 'memo': license_email, 'token': token, 'device_token': device_token, }, ) except ConnectionError: logger.warning( 'Could not connect to license server, please try again later!') return if response.status_code == 404: logger.warning('Valid license NOT found, removing!') remove_license() return if response.status_code == 201: logger.debug('Licence validated!') return try: response.raise_for_status() except HTTPError as e: logger.warning( f'Unexpected status from license server: {e} ({response.text})')
def run_server(): logger.debug(f'Starting server on {SERVER_HOST}:{SERVER_PORT}') try: boot() app.run( host=SERVER_HOST, port=SERVER_PORT, debug=DEBUG, threaded=True, # We can't use the reloader within a thread as it needs signal support use_reloader=False, ) except Exception as e: logger.exception(f'Exception in server thread!: {e}')
def add_contacts(contacts): existing_contacts = get_contact_tuple_to_contact() contacts_to_save = [] for name, email in contacts: if not is_valid_contact(name, email): logger.debug(f'Not saving invalid contact: ({name} {email})') return if (name, email) in existing_contacts: return new_contact = Contact(name=name, email=email) contacts_to_save.append(new_contact) save_contacts(*contacts_to_save)
def get_settings() -> dict: settings = get_default_settings() if path.exists(SETTINGS_FILE): with open(SETTINGS_FILE, 'r') as file: data = file.read() user_settings = json.loads(data) has_changed = fix_any_old_setings(user_settings) if has_changed: set_settings(user_settings) logger.debug(f'Loaded settings: {user_settings}') # Merge the user settings ontop of the defaults _merge_settings(settings, user_settings) return settings
def get_mx_record_domain(domain): logger.debug(f'Fetching MX records for {domain}') name_to_preference = {} names = set() try: for answer in resolver.query(domain, 'MX'): name = get_fld(f'{answer.exchange}'.rstrip('.'), fix_protocol=True) name_to_preference[name] = answer.preference names.add(name) except (resolver.NoAnswer, resolver.NXDOMAIN): return [] return sorted( list(names), key=lambda name: name_to_preference[name], )
def update_device(update) -> None: ''' Checks for and downloads any updates for Kanmail - after this it tells the frontend to render a restart icon. ''' update = update or check_device_update() if not update: return if not FROZEN: logger.warning('App not frozen, not fetching update') return logger.debug(f'Downloading update: {update.version}') update.download() logger.debug('Download complete, extracting & overwriting') update.extract_overwrite()
def _test_account_settings(account_settings, get_folder_settings=False): imap_settings = account_settings['imap_connection'] smtp_settings = account_settings['smtp_connection'] for settings_type, settings in ( ('imap', imap_settings), ('smtp', smtp_settings), ): for key in CONNECTION_KEYS: if not settings.get(key): raise TestAccountSettingsError( settings_type, f'Missing {settings_type.upper()} setting: {key}', ) new_account = Account('Unsaved test account', account_settings) folders = {} # Check IMAP and folders as needed try: with new_account.get_imap_connection() as connection: connection.noop() if get_folder_settings: folders = _get_folder_settings(imap_settings, connection) except Exception as e: trace = traceback.format_exc().strip() logger.debug(f'IMAP connection exception traceback: {trace}') raise TestAccountSettingsError('imap', f'{e}') # Check SMTP try: with new_account.get_smtp_connection() as connection: pass except Exception as e: trace = traceback.format_exc().strip() logger.debug(f'SMTP connection exception traceback: {trace}') raise TestAccountSettingsError('smtp', f'{e}') return folders or True
def create_window( endpoint: str = '/', unique_key: Optional[str] = None, **kwargs, ) -> Union[str, bool]: if not IS_APP: logger.warning('Cannot open window in server mode!') return False internal_id = str(uuid4()) link = (f'http://{SERVER_HOST}:{server.get_port()}{endpoint}' f'?window_id={internal_id}' f'&Kanmail-Session-Token={SESSION_TOKEN}') logger.debug( f'Opening window ({internal_id}) ' f'url={endpoint} kwargs={kwargs}', ) # Nuke any existing unique window if unique_key and unique_key in UNIQUE_NAME_TO_ID: old_window_id = UNIQUE_NAME_TO_ID.get(unique_key) if old_window_id: destroy_window(old_window_id) window = webview.create_window( 'Kanmail', link, frameless=FRAMELESS, easy_drag=False, text_select=True, localization=UNIQUE_KEY_TO_LOCALIZATION.get(unique_key), **kwargs, ) ID_TO_WINDOW[internal_id] = window if unique_key: UNIQUE_NAME_TO_ID[unique_key] = internal_id return internal_id
def boot() -> None: logger.debug(f'App client root is: {CLIENT_ROOT}') if environ.get('KANMAIL_FAKE_IMAP') == 'on': logger.debug('Using fixtures, faking the IMAP client & responses!') from kanmail.server.mail.fake_imap import bootstrap_fake_imap bootstrap_fake_imap() from kanmail import secrets # noqa: F401 from kanmail.server.views import error # noqa: F401 # API views from kanmail.server.views import ( # noqa: F401 accounts_api, contacts_api, email_api, license_api, settings_api, update_api, ) # Database models from kanmail.server.mail.contacts import Contact # noqa: F401 db.create_all()
def get_ispdb_confg(domain): logger.debug(f'Looking up thunderbird autoconfig for {domain}') response = requests.get(f'{ISPDB_URL}/{domain}') if response.status_code == 200: imap_settings = {} smtp_settings = {} # Parse the XML et = parse_xml(response.content) provider = et.find('emailProvider') for incoming in provider.findall('incomingServer'): if incoming.get('type') != 'imap': continue imap_settings['host'] = incoming.find('hostname').text imap_settings['port'] = int(incoming.find('port').text) imap_settings['ssl'] = incoming.find('socketType').text == 'SSL' break for outgoing in provider.findall('outgoingServer'): if outgoing.get('type') != 'smtp': continue smtp_settings['host'] = outgoing.find('hostname').text smtp_settings['port'] = int(outgoing.find('port').text) socket_type = outgoing.find('socketType').text smtp_settings['ssl'] = socket_type == 'SSL' smtp_settings['tls'] = socket_type == 'STARTTLS' break logger.debug(( f'Autoconf settings for {domain}: ' f'imap={imap_settings}, smtp={smtp_settings}' )) return imap_settings, smtp_settings
def get_window_settings() -> dict: settings = { 'width': DEFAULT_WINDOW_WIDTH, 'height': DEFAULT_WINDOW_HEIGHT, 'x': DEFAULT_WINDOW_LEFT, 'y': DEFAULT_WINDOW_TOP, } if path.exists(WINDOW_CACHE_FILE): with open(WINDOW_CACHE_FILE, 'rb') as f: data = pickle.loads(f.read()) logger.debug(f'Loaded window settings: {data}') for key, value in data.items(): if key.startswith('WINDOW_'): # COMPAT w/old style key = key.split('_')[1].lower() logger.warning( f'Updated old window setting: WINDOW_{key} -> {key}') if key in settings: settings[key] = value return settings
def create_window( endpoint: str = '/', unique_key: Optional[str] = None, **kwargs, ) -> Union[str, bool]: if not IS_APP: logger.warning('Cannot open window in server mode!') return False internal_id = str(uuid4()) link = f'http://localhost:{SERVER_PORT}{endpoint}?window_id={internal_id}' logger.debug( f'Opening window (#{internal_id}) ' f'url={endpoint} kwargs={kwargs}', ) # Nuke any existing unique window if unique_key and unique_key in UNIQUE_NAME_TO_ID: old_window_id = UNIQUE_NAME_TO_ID.get(unique_key) if old_window_id: destroy_window(old_window_id) window = webview.create_window( 'Kanmail', link, frameless=FRAMELESS, easy_drag=False, text_select=True, **kwargs, ) ID_TO_WINDOW[internal_id] = window if unique_key: UNIQUE_NAME_TO_ID[unique_key] = internal_id return internal_id
def get_ispdb_confg(domain: str) -> [dict, dict]: if domain in COMMON_ISPDB_DOMAINS: logger.debug(f'Got hardcoded autoconfig for {domain}') return ( COMMON_ISPDB_DOMAINS[domain]['imap_connection'], COMMON_ISPDB_DOMAINS[domain]['smtp_connection'], ) logger.debug(f'Looking up thunderbird autoconfig for {domain}') ispdb_url = ISPDB_URL_FORMATTER.format(domain=domain) try: response = requests.get(ispdb_url) except requests.RequestException as e: logger.warning( f'Failed to fetch ISPDB settings for domain: {domain}: {e}') return if response.status_code == 200: imap_settings = {} smtp_settings = {} # Parse the XML et = parse_xml(response.content) provider = et.find('emailProvider') for incoming in provider.findall('incomingServer'): if incoming.get('type') != 'imap': continue imap_settings['host'] = incoming.find('hostname').text imap_settings['port'] = int(incoming.find('port').text) imap_settings['ssl'] = incoming.find('socketType').text == 'SSL' break for outgoing in provider.findall('outgoingServer'): if outgoing.get('type') != 'smtp': continue smtp_settings['host'] = outgoing.find('hostname').text smtp_settings['port'] = int(outgoing.find('port').text) socket_type = outgoing.find('socketType').text smtp_settings['ssl'] = socket_type == 'SSL' smtp_settings['tls'] = socket_type == 'STARTTLS' break logger.debug((f'Autoconf settings for {domain}: ' f'imap={imap_settings}, smtp={smtp_settings}')) return imap_settings, smtp_settings