mirror of
https://github.com/pulb/mailnag.git
synced 2026-07-04 12:18:24 +02:00
Migrate to libsecret, closes #114
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Copyright 2011 - 2017 Patrick Ulbrich <zulu99@gmx.net>
|
||||
# Copyright 2011 - 2019 Patrick Ulbrich <zulu99@gmx.net>
|
||||
# Copyright 2016 Thomas Haider <t.haider@deprecate.de>
|
||||
# Copyright 2016, 2018 Timo Kankare <timo.kankare@iki.fi>
|
||||
# Copyright 2011 Ralf Hersel <ralf.hersel@gmx.net>
|
||||
@@ -20,7 +20,10 @@
|
||||
#
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
from Mailnag.backends import create_backend, get_mailbox_parameter_specs
|
||||
from Mailnag.common.secretstore import SecretStore
|
||||
from Mailnag.common.dist_cfg import PACKAGE_NAME
|
||||
|
||||
account_defaults = {
|
||||
'enabled' : '0',
|
||||
@@ -36,8 +39,6 @@ account_defaults = {
|
||||
'folder' : '[]'
|
||||
}
|
||||
|
||||
CREDENTIAL_KEY = 'Mailnag password for %s://%s@%s'
|
||||
|
||||
#
|
||||
# Account class
|
||||
#
|
||||
@@ -177,10 +178,13 @@ class Account:
|
||||
# AccountManager class
|
||||
#
|
||||
class AccountManager:
|
||||
def __init__(self, credentialstore = None):
|
||||
def __init__(self):
|
||||
self._accounts = []
|
||||
self._removed = []
|
||||
self._credentialstore = credentialstore
|
||||
self._secretstore = SecretStore.get_default()
|
||||
|
||||
if self._secretstore == None:
|
||||
logging.warning("Failed to create secretstore - account passwords will be stored in plaintext config file.")
|
||||
|
||||
|
||||
def __len__(self):
|
||||
@@ -239,13 +243,13 @@ class AccountManager:
|
||||
option_spec = get_mailbox_parameter_specs(mailbox_type)
|
||||
options = self._get_cfg_options(cfg, section_name, option_spec)
|
||||
|
||||
# TODO: Getting password from credentials is mailbox specific.
|
||||
# Every backend do not have or need password.
|
||||
# TODO: Getting a password from the secretstore is mailbox specific.
|
||||
# Not every backend requires a password.
|
||||
user = options.get('user')
|
||||
server = options.get('server')
|
||||
if self._credentialstore != None and user and server:
|
||||
protocol = 'imap' if imap else 'pop'
|
||||
password = self._credentialstore.get(CREDENTIAL_KEY % (protocol, user, server))
|
||||
if self._secretstore != None and user and server:
|
||||
password = self._secretstore.get(self._get_account_id(user, server, imap))
|
||||
if not password: password = ''
|
||||
options['password'] = password
|
||||
|
||||
acc = Account(enabled=enabled,
|
||||
@@ -267,14 +271,12 @@ class AccountManager:
|
||||
i = i + 1
|
||||
section_name = "account" + str(i)
|
||||
|
||||
# Delete secrets of removed accounts from the credential store
|
||||
# Delete secrets of removed accounts from the secretstore
|
||||
# (it's important to do this before adding accounts,
|
||||
# in case multiple accounts with the same credential key exist).
|
||||
if self._credentialstore != None:
|
||||
# in case multiple accounts with the same id exist).
|
||||
if self._secretstore != None:
|
||||
for acc in self._removed:
|
||||
protocol = 'imap' if acc.imap else 'pop'
|
||||
# Note: CredentialStore implementations must check if the key acutally exists!
|
||||
self._credentialstore.remove(CREDENTIAL_KEY % (protocol, acc.user, acc.server))
|
||||
self._secretstore.remove(self._get_account_id(acc.user, acc.server, acc.imap))
|
||||
|
||||
del self._removed[:]
|
||||
|
||||
@@ -296,18 +298,23 @@ class AccountManager:
|
||||
config = acc.get_config()
|
||||
option_spec = get_mailbox_parameter_specs(acc.mailbox_type)
|
||||
|
||||
# TODO: Setting password to credentials is mailbox specific.
|
||||
# Every backend do not have or need password.
|
||||
if self._credentialstore != None:
|
||||
protocol = 'imap' if acc.imap else 'pop'
|
||||
self._credentialstore.set(CREDENTIAL_KEY % (protocol, acc.user, acc.server), acc.password)
|
||||
# TODO: Storing a password is mailbox specific.
|
||||
# Not every backend requires a password.
|
||||
if self._secretstore != None:
|
||||
self._secretstore.set(self._get_account_id(acc.user, acc.server, acc.imap), acc.password,
|
||||
f'{PACKAGE_NAME.capitalize()} password for account {acc.user}@{acc.server}')
|
||||
config['password'] = ''
|
||||
|
||||
|
||||
self._set_cfg_options(cfg, section_name, config, option_spec)
|
||||
|
||||
i = i + 1
|
||||
|
||||
|
||||
def _get_account_id(self, user, server, is_imap):
|
||||
# TODO : Introduce account.uuid when rewriting account and backend code
|
||||
return hashlib.md5((user + server + str(is_imap)).encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def _get_account_cfg(self, cfg, section_name, option_name):
|
||||
if cfg.has_option(section_name, option_name):
|
||||
return cfg.get(section_name, option_name)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2011 - 2015 Patrick Ulbrich <zulu99@gmx.net>
|
||||
# Copyright 2011 - 2019 Patrick Ulbrich <zulu99@gmx.net>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -27,7 +27,6 @@ mailnag_defaults = {
|
||||
'imap_idle_timeout' : '10',
|
||||
'autostart' : '1',
|
||||
'connectivity_test' : 'auto',
|
||||
'credentialstore' : 'auto',
|
||||
'enabled_plugins' : 'dbusplugin, soundplugin, libnotifyplugin'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
# Copyright 2015 - 2019 Patrick Ulbrich <zulu99@gmx.net>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
# MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
import hashlib
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CredentialStoreType(Enum):
|
||||
NONE = 'none'
|
||||
GNOME = 'gnome'
|
||||
# KDE = 'kde'
|
||||
|
||||
|
||||
_credentialstoretype = CredentialStoreType.NONE
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('GnomeKeyring', '1.0')
|
||||
from gi.repository import GnomeKeyring
|
||||
_credentialstoretype = CredentialStoreType.GNOME
|
||||
except: pass
|
||||
|
||||
|
||||
#
|
||||
# CredentialStore base class
|
||||
#
|
||||
class CredentialStore:
|
||||
_instance = None
|
||||
|
||||
def set(self, key, secret):
|
||||
pass
|
||||
|
||||
|
||||
def get(self, key):
|
||||
pass
|
||||
|
||||
|
||||
def remove(self, key):
|
||||
pass
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_default():
|
||||
if (CredentialStore._instance == None) and (_credentialstoretype != CredentialStoreType.NONE):
|
||||
CredentialStore._instance = SupportedCredentialStores[_credentialstoretype]()
|
||||
|
||||
return CredentialStore._instance
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_string(strn):
|
||||
cs = None
|
||||
if strn == 'auto':
|
||||
cs = CredentialStore.get_default()
|
||||
elif strn in SupportedCredentialStores:
|
||||
cs = SupportedCredentialStores[strn]()
|
||||
|
||||
return cs
|
||||
|
||||
|
||||
#
|
||||
# GNOME CredentialStore
|
||||
#
|
||||
class GnomeCredentialStore(CredentialStore):
|
||||
def __init__(self):
|
||||
(result, kr_name) = GnomeKeyring.get_default_keyring_sync()
|
||||
self._defaultKeyring = kr_name
|
||||
|
||||
if self._defaultKeyring == None:
|
||||
self._defaultKeyring = 'login'
|
||||
|
||||
result = GnomeKeyring.unlock_sync(self._defaultKeyring, None)
|
||||
|
||||
if result != GnomeKeyring.Result.OK:
|
||||
raise KeyringUnlockException('Failed to unlock default keyring')
|
||||
|
||||
self._migrate_keyring()
|
||||
|
||||
|
||||
def get(self, key):
|
||||
attrs = self._get_attrs(key)
|
||||
result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs)
|
||||
|
||||
if result == GnomeKeyring.Result.OK:
|
||||
return items[0].secret
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
def set(self, key, secret):
|
||||
if secret == '':
|
||||
return
|
||||
|
||||
attrs = self._get_attrs(key)
|
||||
|
||||
existing_secret = ''
|
||||
result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs)
|
||||
|
||||
if result == GnomeKeyring.Result.OK:
|
||||
existing_secret = items[0].secret
|
||||
|
||||
if existing_secret != secret:
|
||||
GnomeKeyring.item_create_sync(self._defaultKeyring, \
|
||||
GnomeKeyring.ItemType.GENERIC_SECRET, key, \
|
||||
attrs, secret, True)
|
||||
|
||||
|
||||
def remove(self, key):
|
||||
attrs = self._get_attrs(key)
|
||||
result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs)
|
||||
|
||||
if result == GnomeKeyring.Result.OK:
|
||||
GnomeKeyring.item_delete_sync(self._defaultKeyring, items[0].item_id)
|
||||
|
||||
|
||||
def _get_attrs(self, key):
|
||||
attrs = GnomeKeyring.Attribute.list_new()
|
||||
keyid = hashlib.md5(key.encode('utf-8')).hexdigest()
|
||||
GnomeKeyring.Attribute.list_append_string(attrs, 'source', 'Mailnag')
|
||||
GnomeKeyring.Attribute.list_append_string(attrs, 'api-version', '1.1')
|
||||
GnomeKeyring.Attribute.list_append_string(attrs, 'keyid', keyid)
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
# Migrates pre Mailnag 1.1 keyring items into the new format
|
||||
def _migrate_keyring(self):
|
||||
attrs = GnomeKeyring.Attribute.list_new()
|
||||
GnomeKeyring.Attribute.list_append_string(attrs, 'application', 'Mailnag')
|
||||
result, items = GnomeKeyring.find_items_sync(GnomeKeyring.ItemType.GENERIC_SECRET, attrs)
|
||||
|
||||
if result == GnomeKeyring.Result.OK:
|
||||
for i in items:
|
||||
result, info = GnomeKeyring.item_get_info_sync(self._defaultKeyring, i.item_id)
|
||||
self.set(info.get_display_name(), i.secret)
|
||||
GnomeKeyring.item_delete_sync(self._defaultKeyring, i.item_id)
|
||||
|
||||
|
||||
#
|
||||
# Exception thrown if the GNOME keyring can't be unlocked
|
||||
#
|
||||
class KeyringUnlockException(Exception):
|
||||
def __init__(self, message):
|
||||
Exception.__init__(self, message)
|
||||
|
||||
|
||||
#
|
||||
# All supported credential stores
|
||||
#
|
||||
SupportedCredentialStores = {
|
||||
CredentialStoreType.GNOME : GnomeCredentialStore
|
||||
#CredentialStoreType.KDE : KDECredentialStore
|
||||
}
|
||||
66
Mailnag/common/secretstore.py
Normal file
66
Mailnag/common/secretstore.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2019 Patrick Ulbrich <zulu99@gmx.net>
|
||||
# Copyright 2019 razer <razerraz@free.fr>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||||
# MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
from Mailnag.common.dist_cfg import PACKAGE_NAME
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version('Secret', '1')
|
||||
from gi.repository import Secret
|
||||
_libsecret_err = None
|
||||
except ModuleNotFoundError as e:
|
||||
_libsecret_err = e
|
||||
|
||||
|
||||
|
||||
class SecretStore():
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
if _libsecret_err != None:
|
||||
raise _libsecret_err
|
||||
|
||||
self._schema = Secret.Schema.new(
|
||||
f'com.github.pulb.{PACKAGE_NAME}', Secret.SchemaFlags.NONE,
|
||||
{'id' : Secret.SchemaAttributeType.STRING})
|
||||
|
||||
|
||||
def get(self, secret_id):
|
||||
return Secret.password_lookup_sync(self._schema, {'id': secret_id}, None)
|
||||
|
||||
|
||||
def set(self, secret_id, secret, description):
|
||||
Secret.password_store_sync(
|
||||
self._schema, {'id': secret_id}, Secret.COLLECTION_DEFAULT,
|
||||
description, secret, None)
|
||||
|
||||
|
||||
def remove(self, secret_id):
|
||||
return Secret.password_clear_sync(self._schema, {'id': secret_id}, None)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_default():
|
||||
if _libsecret_err != None:
|
||||
return None
|
||||
|
||||
if SecretStore._instance == None:
|
||||
SecretStore._instance = SecretStore()
|
||||
|
||||
return SecretStore._instance
|
||||
@@ -30,7 +30,6 @@ from Mailnag.common.i18n import _
|
||||
from Mailnag.common.utils import get_data_file
|
||||
from Mailnag.common.config import read_cfg, write_cfg
|
||||
from Mailnag.common.accounts import Account, AccountManager
|
||||
from Mailnag.common.credentialstore import CredentialStore
|
||||
from Mailnag.common.plugins import Plugin
|
||||
from Mailnag.configuration.accountdialog import AccountDialog
|
||||
from Mailnag.configuration.plugindialog import PluginDialog
|
||||
@@ -67,7 +66,7 @@ class ConfigWindow:
|
||||
#
|
||||
# accounts page
|
||||
#
|
||||
self._accountman = AccountManager(CredentialStore.from_string(self._cfg.get('core', 'credentialstore')))
|
||||
self._accountman = AccountManager()
|
||||
|
||||
self._treeview_accounts = builder.get_object("treeview_accounts")
|
||||
self._liststore_accounts = builder.get_object("liststore_accounts")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Copyright 2016 Timo Kankare <timo.kankare@iki.fi>
|
||||
# Copyright 2014, 2015 Patrick Ulbrich <zulu99@gmx.net>
|
||||
# Copyright 2014 - 2019 Patrick Ulbrich <zulu99@gmx.net>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -22,7 +22,6 @@ import logging
|
||||
import time
|
||||
|
||||
from Mailnag.common.accounts import AccountManager
|
||||
from Mailnag.common.credentialstore import CredentialStore
|
||||
from Mailnag.daemon.mailchecker import MailChecker
|
||||
from Mailnag.daemon.mails import Memorizer
|
||||
from Mailnag.daemon.idlers import IdlerRunner
|
||||
@@ -73,7 +72,7 @@ class MailnagDaemon:
|
||||
|
||||
self._cfg = read_cfg()
|
||||
|
||||
accountman = AccountManager(CredentialStore.from_string(self._cfg.get('core', 'credentialstore')))
|
||||
accountman = AccountManager()
|
||||
accountman.load_from_cfg(self._cfg, enabled_only = True)
|
||||
self._accounts = accountman.to_list()
|
||||
self._hookreg = HookRegistry()
|
||||
|
||||
Reference in New Issue
Block a user