Пример #1
0
def save_contact(contact):
    logger.debug(f'Saving contact: {contact}')

    db.session.add(contact)
    db.session.commit()

    get_contacts.cache_clear()
Пример #2
0
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}')
Пример #3
0
    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
Пример #4
0
    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()
Пример #5
0
def delete_contact(contact):
    logger.debug(f'Deleting contact: {contact}')

    db.session.delete(contact)
    db.session.commit()

    get_contacts.cache_clear()
Пример #6
0
    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)
Пример #7
0
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()
Пример #8
0
    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,
        }
Пример #9
0
    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),
        }
Пример #10
0
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()
Пример #11
0
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 = {}
Пример #12
0
    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
Пример #13
0
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)
Пример #14
0
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))
Пример #15
0
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()
Пример #16
0
    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)
Пример #17
0
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()
Пример #18
0
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})')
Пример #19
0
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}')
Пример #20
0
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)
Пример #21
0
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
Пример #22
0
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],
    )
Пример #23
0
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()
Пример #24
0
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
Пример #25
0
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
Пример #26
0
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()
Пример #27
0
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
Пример #28
0
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
Пример #29
0
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
Пример #30
0
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