From 414f958cd7e33e24c0dc36eb1649b6613f4ff546 Mon Sep 17 00:00:00 2001 From: mbugeia Date: Mon, 12 Sep 2016 13:44:44 +0200 Subject: [PATCH] [enh] Add initial alias module --- data/actionsmap/yunohost.yml | 113 +++++++++++++++++ data/other/ldap_scheme.yml | 6 + locales/en.json | 14 ++- src/yunohost/alias.py | 238 +++++++++++++++++++++++++++++++++++ 4 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/alias.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5a1465258..97fb68e9d 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -226,6 +226,119 @@ user: username: help: Username or email to get information +############################# +# Alias # +############################# +alias: + category_help: Manage alias + actions: + + ### alias_init() + init: + action_help: Init alias + api: GET /alias/init + configuration: + authenticate: all + + ### alias_list() + list: + action_help: List alias + api: GET /alias + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + --fields: + help: fields to fetch + nargs: "+" + -f: + full: --filter + help: LDAP filter used to search + -l: + full: --limit + help: Maximum number of alias fetched + type: int + -o: + full: --offset + help: Starting number for alias fetching + type: int + + ### alias_create() + create: + action_help: Create alias + api: POST /alias + configuration: + authenticate: all + arguments: + alias: + help: The unique alias name to create + extra: + required: True + pattern: *pattern_email + -n: + full: --name + extra: + ask: ask_alias_name + help: A name for the alias + required: True + pattern: &pattern_name + - !!str ^([^\W\d_]{2,30}[ ,.'-]{0,3})+$ + - "pattern_name" + -f: + full: --mailforward + help: List of email to forward, separated by commas without space + extra: + ask: ask_alias_mailforward + required: True + pattern: &pattern_listemail + - !!str ^([\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+([^\W\d_]{2,}),?)+$ + - "pattern_listemail" + + ### alias_delete() + delete: + action_help: Delete alias + api: DELETE /alias/ + configuration: + authenticate: all + arguments: + alias: + help: Alias to delete + extra: + pattern: *pattern_email + + ### alias_update() + update: + action_help: Update alias informations + api: PUT /alias/ + configuration: + authenticate: all + arguments: + alias: + help: Alias to update + -n: + full: --name + help: Unique name for the alias + extra: + pattern: *pattern_name + --add-mailforward: + help: List of mailforward addresses to add, separated by commas without space + extra: + pattern: *pattern_listemail + --remove-mailforward: + help: List of mailforward addresses to remove, separated by commas without space + extra: + pattern: *pattern_listemail + + ### alias_info() + info: + action_help: Get alias information + api: GET /alias/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + alias: + help: Alias to get information ############################# # Domain # diff --git a/data/other/ldap_scheme.yml b/data/other/ldap_scheme.yml index 75bdea6e2..e10ee1855 100644 --- a/data/other/ldap_scheme.yml +++ b/data/other/ldap_scheme.yml @@ -28,6 +28,12 @@ parents: - organizationalUnit - top + ou=aliases: + ou: aliases + objectClass: + - organizationalUnit + - top + children: cn=admins,ou=groups: cn: admins diff --git a/locales/en.json b/locales/en.json index e939b26fa..4cd1e85cc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -237,5 +237,17 @@ "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", + "ask_alias_name" : "Alias name", + "ask_alias_mailforward" : "List of email to forward", + "pattern_listemail" : "Must be a valid email address list delimited by commas", + "pattern_name" : "Must be a valid name", + "alias_created" : "Alias successfully created", + "alias_creation_failed" : "Unable to create alias", + "alias_deletion_failed" : "Unable to delete alias", + "alias_deleted" : "Alias successfully deleted", + "alias_unknown" : "Unknown alias", + "alias_updated" : "Alias successfully updated", + "alias_update_failed" : "Unable to update alias", + "alias_info_failed" : "Unable to retrieve alias information" } diff --git a/src/yunohost/alias.py b/src/yunohost/alias.py new file mode 100644 index 000000000..f3a0ce1f0 --- /dev/null +++ b/src/yunohost/alias.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_alias.py + + Manage alias +""" + +import errno + +from moulinette.core import MoulinetteError + + +def alias_init(auth): + """ + Init alias schema, workaround to activate alias on an existing install, better solution needed + """ + rdn = 'ou=alias' + attr_dict = { + 'objectClass' : ['organizationalUnit', 'top'], + } + if auth.add(rdn, attr_dict): + msignals.display(m18n.n('alias_init'), 'success') + +def alias_list(auth, fields=None, filter=None, limit=None, offset=None): + """ + List alias + + Keyword argument: + filter -- LDAP filter used to search + offset -- Starting number for alias fetching + limit -- Maximum number of alias fetched + fields -- fields to fetch + + """ + alias_attrs = { 'mail': 'alias', + 'cn': 'name', + 'maildrop': 'mail-forward'} + attrs = [] + result_list = [] + + # Set default arguments values + if offset is None: + offset = 0 + if limit is None: + limit = 1000 + if filter is None: + filter = '(&(objectclass=mailAccount)(!(uid=root))(!(uid=nobody)))' + if fields: + keys = alias_attrs.keys() + for attr in fields: + if attr in keys: + attrs.append(attr) + else: + raise MoulinetteError(errno.EINVAL, + m18n.n('field_invalid', attr)) + else: + attrs = [ 'mail', 'cn', 'maildrop' ] + + result = auth.search('ou=alias,dc=yunohost,dc=org', filter, attrs) + + if len(result) > offset and limit > 0: + for alias in result[offset:offset+limit]: + entry = {} + for attr, values in alias.items(): + try: + entry[alias_attrs[attr]] = values[0:] + except: + pass + result_list.append(entry) + return { 'alias' : result_list } + + +def alias_create(auth, alias, name, mailforward): + """ + Create alias + + Keyword argument: + name -- + alias -- Main mail address must be unique + mailforward -- List of email to forward, separated by commas without space + + """ + from yunohost.domain import domain_list + + # Validate uniqueness of alias and mail in LDAP + auth.validate_uniqueness({ + 'uid' : alias, + 'mail' : alias + }) + + # Check that the mail domain exists + if alias[alias.find('@')+1:] not in domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, + m18n.n('mail_domain_unknown', + alias[alias.find('@')+1:])) + + # Adapt values for LDAP + rdn = 'uid=%s,ou=alias' % alias + attr_dict = { + 'objectClass' : ['mailAccount', 'inetOrgPerson'], + 'sn' : alias, + 'displayName' : name, + 'cn' : name, + 'uid' : alias, + 'mail' : alias + } + + attr_dict['maildrop'] = mailforward.split(",") + + if auth.add(rdn, attr_dict): + msignals.display(m18n.n('alias_created'), 'success') + return { 'alias' : alias, 'name' : name, 'mailforward' : attr_dict['maildrop'] } + + raise MoulinetteError(169, m18n.n('alias_creation_failed')) + + +def alias_delete(auth, alias): + """ + Delete alias + + Keyword argument: + alias -- Alias to delete + + """ + + if auth.remove('uid=%s,ou=alias' % alias): + pass + else: + raise MoulinetteError(169, m18n.n('alias_deletion_failed')) + + msignals.display(m18n.n('alias_deleted'), 'success') + + +def alias_update(auth, alias, name=None, add_mailforward=None, remove_mailforward=None): + """ + Update alias informations + + Keyword argument: + alias + name + remove_mailforward -- Mailforward addresses to remove + add_mailforward -- Mailforward addresses to add + + """ + + attrs_to_fetch = ['uid', 'cn', 'displayName', 'maildrop'] + new_attr_dict = {} + + # Populate alias informations + result = auth.search(base='ou=alias,dc=yunohost,dc=org', filter='uid=' + alias, attrs=attrs_to_fetch) + if not result: + raise MoulinetteError(errno.EINVAL, m18n.n('alias_unknown')) + alias_fetched = result[0] + + # Get modifications from arguments + if name: + new_attr_dict['cn'] = name + new_attr_dict['displayName'] = name + + if add_mailforward: + add_mailforward = add_mailforward.split(",") + for mail in add_mailforward: + if mail in alias_fetched['maildrop'][1:]: + continue + alias_fetched['maildrop'].append(mail) + new_attr_dict['maildrop'] = alias_fetched['maildrop'] + + if remove_mailforward: + remove_mailforward = remove_mailforward.split(",") + for mail in remove_mailforward: + if mail not in alias_fetched['maildrop'][1:]: + continue + alias_fetched['maildrop'].remove(mail) + new_attr_dict['maildrop'] = alias_fetched['maildrop'] + + if auth.update('uid=%s,ou=alias' % alias, new_attr_dict): + msignals.display(m18n.n('alias_updated'), 'success') + return alias_info(auth, alias) + else: + raise MoulinetteError(169, m18n.n('alias_update_failed')) + + +def alias_info(auth, alias): + """ + Get alias informations + + Keyword argument: + alias -- Alias mail to get informations + + """ + alias_attrs = [ + 'cn', 'mail', 'uid', 'maildrop', 'givenName', 'sn' + ] + + if len(alias.split('@')) is 2: + filter = 'mail=' + alias + else: + filter = 'uid=' + alias + + result = auth.search('ou=alias,dc=yunohost,dc=org', filter, alias_attrs) + + if result: + alias = result[0] + else: + raise MoulinetteError(errno.EINVAL, m18n.n('alias_unknown')) + + result_dict = { + 'alias': alias['mail'][0], + 'name': alias['cn'][0], + } + + if len(alias['maildrop']) > 1: + result_dict['mail-forward'] = alias['maildrop'][0:] + + if result: + return result_dict + else: + raise MoulinetteError(167, m18n.n('alias_info_failed')) +