From 9bf1bac7df54f5be9cdca9a5d7a36160c40e25dd Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Fri, 29 Jul 2016 17:39:13 +0300 Subject: [PATCH] Rewrite mod_vcard_ldap to use XML generator --- src/ejabberd_config.erl | 1 + src/gen_mod.erl | 24 +- src/mod_vcard.erl | 80 ++--- src/mod_vcard_ldap.erl | 736 ++++++++++----------------------------- src/mod_vcard_mnesia.erl | 55 ++- src/mod_vcard_riak.erl | 11 +- src/mod_vcard_sql.erl | 52 ++- 7 files changed, 335 insertions(+), 624 deletions(-) diff --git a/src/ejabberd_config.erl b/src/ejabberd_config.erl index 87a918704..5a39df043 100644 --- a/src/ejabberd_config.erl +++ b/src/ejabberd_config.erl @@ -1010,6 +1010,7 @@ replace_module(mod_private_odbc) -> {mod_private, sql}; replace_module(mod_roster_odbc) -> {mod_roster, sql}; replace_module(mod_shared_roster_odbc) -> {mod_shared_roster, sql}; replace_module(mod_vcard_odbc) -> {mod_vcard, sql}; +replace_module(mod_vcard_ldap) -> {mod_vcard, ldap}; replace_module(mod_vcard_xupdate_odbc) -> {mod_vcard_xupdate, sql}; replace_module(mod_pubsub_odbc) -> {mod_pubsub, sql}; replace_module(Module) -> diff --git a/src/gen_mod.erl b/src/gen_mod.erl index c4306577c..e5b504897 100644 --- a/src/gen_mod.erl +++ b/src/gen_mod.erl @@ -48,7 +48,7 @@ opts = [] :: opts() | '_' | '$2'}). -type opts() :: [{atom(), any()}]. --type db_type() :: sql | mnesia | riak. +-type db_type() :: sql | mnesia | riak | ldap. -callback start(binary(), opts()) -> any(). -callback stop(binary()) -> any(). @@ -147,7 +147,7 @@ start_module(Host, Module) -> -spec start_module(binary(), atom(), opts()) -> any(). start_module(Host, Module, Opts0) -> - Opts = validate_opts(Module, Opts0), + Opts = validate_opts(Host, Module, Opts0), ets:insert(ejabberd_modules, #ejabberd_module{module_host = {Module, Host}, opts = Opts}), @@ -308,10 +308,10 @@ get_opt_host(Host, Opts, Default) -> Val = get_opt(host, Opts, fun iolist_to_binary/1, Default), ejabberd_regexp:greplace(Val, <<"@HOST@">>, Host). -validate_opts(Module, Opts) -> +validate_opts(Host, Module, Opts) -> lists:filtermap( fun({Opt, Val}) -> - case catch Module:mod_opt_type(Opt) of + case catch validate_opt(Host, Module, Opt, Opts) of VFun when is_function(VFun) -> try VFun(Val) of _ -> @@ -346,6 +346,22 @@ validate_opts(Module, Opts) -> false end, Opts). +validate_opt(Host, Module, Opt, Opts) -> + case Module:mod_opt_type(Opt) of + VFun1 when is_function(VFun1) -> + VFun1; + L1 when is_list(L1) -> + DBModule = db_mod(Host, Opts, Module), + try DBModule:mod_opt_type(Opt) of + VFun2 when is_function(VFun2) -> + VFun2; + L2 when is_list(L2) -> + lists:usort(L1 ++ L2) + catch _:undef -> + L1 + end + end. + -spec db_type(binary() | global, module()) -> db_type(); (opts(), module()) -> db_type(). diff --git a/src/mod_vcard.erl b/src/mod_vcard.erl index b75b6575b..231c42dc0 100644 --- a/src/mod_vcard.erl +++ b/src/mod_vcard.erl @@ -49,12 +49,15 @@ -define(PROCNAME, ejabberd_mod_vcard). -callback init(binary(), gen_mod:opts()) -> any(). +-callback stop(binary()) -> any(). -callback import(binary(), #vcard{} | #vcard_search{}) -> ok | pass. -callback get_vcard(binary(), binary()) -> [xmlel()] | error. -callback set_vcard(binary(), binary(), xmlel(), #vcard_search{}) -> {atomic, any()}. +-callback search_fields(binary()) -> [{binary(), binary()}]. +-callback search_reported(binary()) -> [{binary(), binary()}]. -callback search(binary(), [{binary(), [binary()]}], boolean(), - infinity | pos_integer()) -> [binary()]. + infinity | pos_integer()) -> [{binary(), binary()}]. -callback remove_user(binary(), binary()) -> {atomic, any()}. start(Host, Opts) -> @@ -134,6 +137,8 @@ stop(Host) -> ?NS_VCARD), ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + Mod = gen_mod:db_type(Host, ?MODULE), + Mod:stop(Host), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), Proc ! stop, {wait, Proc}. @@ -214,7 +219,8 @@ process_vcard(#iq{type = get, lang = Lang} = IQ) -> -spec process_search(iq()) -> iq(). process_search(#iq{type = get, to = To, lang = Lang} = IQ) -> - xmpp:make_iq_result(IQ, mk_search_form(To, Lang)); + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + xmpp:make_iq_result(IQ, mk_search_form(To, ServerHost, Lang)); process_search(#iq{type = set, to = To, lang = Lang, sub_els = [#search{xdata = #xdata{type = submit, fields = Fs}}]} = IQ) -> @@ -366,22 +372,13 @@ mk_tfield(Label, Var, Lang) -> mk_field(Var, Val) -> #xdata_field{var = Var, values = [Val]}. --spec mk_search_form(jid(), undefined | binary()) -> search(). -mk_search_form(JID, Lang) -> +-spec mk_search_form(jid(), binary(), undefined | binary()) -> search(). +mk_search_form(JID, ServerHost, Lang) -> Title = <<(translate:translate(Lang, <<"Search users in ">>))/binary, (jid:to_string(JID))/binary>>, - Fs = [mk_tfield(<<"User">>, <<"user">>, Lang), - mk_tfield(<<"Full Name">>, <<"fn">>, Lang), - mk_tfield(<<"Name">>, <<"first">>, Lang), - mk_tfield(<<"Middle Name">>, <<"middle">>, Lang), - mk_tfield(<<"Family Name">>, <<"last">>, Lang), - mk_tfield(<<"Nickname">>, <<"nick">>, Lang), - mk_tfield(<<"Birthday">>, <<"bday">>, Lang), - mk_tfield(<<"Country">>, <<"ctry">>, Lang), - mk_tfield(<<"City">>, <<"locality">>, Lang), - mk_tfield(<<"Email">>, <<"email">>, Lang), - mk_tfield(<<"Organization Name">>, <<"orgname">>, Lang), - mk_tfield(<<"Organization Unit">>, <<"orgunit">>, Lang)], + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + SearchFields = Mod:search_fields(ServerHost), + Fs = [mk_tfield(Label, Var, Lang) || {Label, Var} <- SearchFields], X = #xdata{type = form, title = Title, instructions = @@ -398,55 +395,20 @@ mk_search_form(JID, Lang) -> -spec search_result(undefined | binary(), jid(), binary(), [xdata_field()]) -> xdata(). search_result(Lang, JID, ServerHost, XFields) -> + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + Reported = [mk_tfield(Label, Var, Lang) || + {Label, Var} <- Mod:search_reported(ServerHost)], #xdata{type = result, title = <<(translate:translate(Lang, <<"Search Results for ">>))/binary, (jid:to_string(JID))/binary>>, - reported = [mk_tfield(<<"Jabber ID">>, <<"jid">>, Lang), - mk_tfield(<<"Full Name">>, <<"fn">>, Lang), - mk_tfield(<<"Name">>, <<"first">>, Lang), - mk_tfield(<<"Middle Name">>, <<"middle">>, Lang), - mk_tfield(<<"Family Name">>, <<"last">>, Lang), - mk_tfield(<<"Nickname">>, <<"nick">>, Lang), - mk_tfield(<<"Birthday">>, <<"bday">>, Lang), - mk_tfield(<<"Country">>, <<"ctry">>, Lang), - mk_tfield(<<"City">>, <<"locality">>, Lang), - mk_tfield(<<"Email">>, <<"email">>, Lang), - mk_tfield(<<"Organization Name">>, <<"orgname">>, Lang), - mk_tfield(<<"Organization Unit">>, <<"orgunit">>, Lang)], - items = lists:map(fun (R) -> record_to_item(ServerHost, R) end, + reported = Reported, + items = lists:map(fun (Item) -> item_to_field(Item) end, search(ServerHost, XFields))}. --spec record_to_item(binary(), [binary()] | #vcard_search{}) -> [xdata_field()]. -record_to_item(LServer, - [Username, FN, Family, Given, Middle, Nickname, BDay, - CTRY, Locality, EMail, OrgName, OrgUnit]) -> - [mk_field(<<"jid">>, <>), - mk_field(<<"fn">>, FN), - mk_field(<<"last">>, Family), - mk_field(<<"first">>, Given), - mk_field(<<"middle">>, Middle), - mk_field(<<"nick">>, Nickname), - mk_field(<<"bday">>, BDay), - mk_field(<<"ctry">>, CTRY), - mk_field(<<"locality">>, Locality), - mk_field(<<"email">>, EMail), - mk_field(<<"orgname">>, OrgName), - mk_field(<<"orgunit">>, OrgUnit)]; -record_to_item(_LServer, #vcard_search{} = R) -> - {User, Server} = R#vcard_search.user, - [mk_field(<<"jid">>, <>), - mk_field(<<"fn">>, (R#vcard_search.fn)), - mk_field(<<"last">>, (R#vcard_search.family)), - mk_field(<<"first">>, (R#vcard_search.given)), - mk_field(<<"middle">>, (R#vcard_search.middle)), - mk_field(<<"nick">>, (R#vcard_search.nickname)), - mk_field(<<"bday">>, (R#vcard_search.bday)), - mk_field(<<"ctry">>, (R#vcard_search.ctry)), - mk_field(<<"locality">>, (R#vcard_search.locality)), - mk_field(<<"email">>, (R#vcard_search.email)), - mk_field(<<"orgname">>, (R#vcard_search.orgname)), - mk_field(<<"orgunit">>, (R#vcard_search.orgunit))]. +-spec item_to_field([{binary(), binary()}]) -> [xdata_field()]. +item_to_field(Items) -> + [mk_field(Var, Value) || {Var, Value} <- Items]. -spec search(binary(), [xdata_field()]) -> [binary()]. search(LServer, XFields) -> diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index a0ad305a9..74b20d2ff 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -1,53 +1,30 @@ -%%%---------------------------------------------------------------------- -%%% File : mod_vcard_ldap.erl -%%% Author : Alexey Shchepin -%%% Purpose : Support for VCards from LDAP storage. -%%% Created : 2 Jan 2003 by Alexey Shchepin +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc %%% -%%% -%%% ejabberd, Copyright (C) 2002-2016 ProcessOne -%%% -%%% 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. -%%% -%%%---------------------u------------------------------------------------- - +%%% @end +%%% Created : 29 Jul 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- -module(mod_vcard_ldap). --behaviour(ejabberd_config). - --author('alexey@process-one.net'). - -behaviour(gen_server). +-behaviour(mod_vcard). --behaviour(gen_mod). +%% API +-export([start_link/2]). +-export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, + remove_user/2, import/2, search_fields/1, search_reported/1, + mod_opt_type/1, opt_type/1]). -%% gen_server callbacks. --export([init/1, handle_info/2, handle_call/3, - handle_cast/2, terminate/2, code_change/3]). - --export([start/2, start_link/2, stop/1, - get_sm_features/5, process_local_iq/3, process_sm_iq/3, - remove_user/1, route/4, transform_module_options/1, - mod_opt_type/1, opt_type/1, depends/2]). +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). -include("ejabberd.hrl"). -include("logger.hrl"). - -include("eldap.hrl"). - --include("jlib.hrl"). +-include("xmpp.hrl"). -define(PROCNAME, ejabberd_mod_vcard_ldap). @@ -74,59 +51,14 @@ deref_aliases = never :: never | searching | finding | always, matches = 0 :: non_neg_integer()}). --define(VCARD_MAP, - [{<<"NICKNAME">>, <<"%u">>, []}, - {<<"FN">>, <<"%s">>, [<<"displayName">>]}, - {<<"FAMILY">>, <<"%s">>, [<<"sn">>]}, - {<<"GIVEN">>, <<"%s">>, [<<"givenName">>]}, - {<<"MIDDLE">>, <<"%s">>, [<<"initials">>]}, - {<<"ORGNAME">>, <<"%s">>, [<<"o">>]}, - {<<"ORGUNIT">>, <<"%s">>, [<<"ou">>]}, - {<<"CTRY">>, <<"%s">>, [<<"c">>]}, - {<<"LOCALITY">>, <<"%s">>, [<<"l">>]}, - {<<"STREET">>, <<"%s">>, [<<"street">>]}, - {<<"REGION">>, <<"%s">>, [<<"st">>]}, - {<<"PCODE">>, <<"%s">>, [<<"postalCode">>]}, - {<<"TITLE">>, <<"%s">>, [<<"title">>]}, - {<<"URL">>, <<"%s">>, [<<"labeleduri">>]}, - {<<"DESC">>, <<"%s">>, [<<"description">>]}, - {<<"TEL">>, <<"%s">>, [<<"telephoneNumber">>]}, - {<<"EMAIL">>, <<"%s">>, [<<"mail">>]}, - {<<"BDAY">>, <<"%s">>, [<<"birthDay">>]}, - {<<"ROLE">>, <<"%s">>, [<<"employeeType">>]}, - {<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}]). +%%%=================================================================== +%%% API +%%%=================================================================== +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). --define(SEARCH_FIELDS, - [{<<"User">>, <<"%u">>}, - {<<"Full Name">>, <<"displayName">>}, - {<<"Given Name">>, <<"givenName">>}, - {<<"Middle Name">>, <<"initials">>}, - {<<"Family Name">>, <<"sn">>}, - {<<"Nickname">>, <<"%u">>}, - {<<"Birthday">>, <<"birthDay">>}, - {<<"Country">>, <<"c">>}, {<<"City">>, <<"l">>}, - {<<"Email">>, <<"mail">>}, - {<<"Organization Name">>, <<"o">>}, - {<<"Organization Unit">>, <<"ou">>}]). - --define(SEARCH_REPORTED, - [{<<"Full Name">>, <<"FN">>}, - {<<"Given Name">>, <<"FIRST">>}, - {<<"Middle Name">>, <<"MIDDLE">>}, - {<<"Family Name">>, <<"LAST">>}, - {<<"Nickname">>, <<"NICK">>}, - {<<"Birthday">>, <<"BDAY">>}, - {<<"Country">>, <<"CTRY">>}, - {<<"City">>, <<"LOCALITY">>}, - {<<"Email">>, <<"EMAIL">>}, - {<<"Organization Name">>, <<"ORGNAME">>}, - {<<"Organization Unit">>, <<"ORGUNIT">>}]). - -handle_cast(_Request, State) -> {noreply, State}. - -code_change(_OldVsn, State, _Extra) -> {ok, State}. - -start(Host, Opts) -> +init(Host, Opts) -> Proc = gen_mod:get_module_proc(Host, ?PROCNAME), ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, transient, 1000, worker, [?MODULE]}, @@ -138,141 +70,127 @@ stop(Host) -> supervisor:terminate_child(ejabberd_sup, Proc), supervisor:delete_child(ejabberd_sup, Proc). -depends(_Host, _Opts) -> - []. - -terminate(_Reason, State) -> - Host = State#state.serverhost, - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, - ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, - ?NS_VCARD), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), - case State#state.search of - true -> - ejabberd_router:unregister_route(State#state.myhost); - _ -> ok +get_vcard(LUser, LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + VCardMap = State#state.vcard_map, + case find_ldap_user(LUser, State) of + #eldap_entry{attributes = Attributes} -> + ldap_attributes_to_vcard(Attributes, VCardMap, + {LUser, LServer}); + _ -> + [] end. -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, - [Host, Opts], []). +set_vcard(_LUser, _LServer, _VCard, _VCardSearch) -> + {atomic, not_implemented}. +search_fields(LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + State#state.search_fields. + +search_reported(LServer) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + State#state.search_reported. + +search(LServer, Data, _AllowReturnAll, MaxMatch) -> + {ok, State} = eldap_utils:get_state(LServer, ?PROCNAME), + Base = State#state.base, + SearchFilter = State#state.search_filter, + Eldap_ID = State#state.eldap_id, + UIDs = State#state.uids, + ReportedAttrs = State#state.search_reported_attrs, + Filter = eldap:'and'([SearchFilter, + eldap_utils:make_filter(Data, UIDs)]), + case eldap_pool:search(Eldap_ID, + [{base, Base}, {filter, Filter}, {limit, MaxMatch}, + {deref_aliases, State#state.deref_aliases}, + {attributes, ReportedAttrs}]) + of + #eldap_search_result{entries = E} -> + search_items(E, State); + _ -> + [] + end. + +search_items(Entries, State) -> + LServer = State#state.serverhost, + SearchReported = State#state.search_reported, + VCardMap = State#state.vcard_map, + UIDs = State#state.uids, + Attributes = lists:map(fun (E) -> + #eldap_entry{attributes = Attrs} = E, Attrs + end, + Entries), + lists:flatmap( + fun(Attrs) -> + case eldap_utils:find_ldap_attrs(UIDs, Attrs) of + {U, UIDAttrFormat} -> + case eldap_utils:get_user_part(U, UIDAttrFormat) of + {ok, Username} -> + case ejabberd_auth:is_user_exists(Username, + LServer) of + true -> + RFields = lists:map( + fun({_, VCardName}) -> + {VCardName, + map_vcard_attr(VCardName, + Attrs, + VCardMap, + {Username, + ?MYNAME})} + end, + SearchReported), + J = <>, + [{<<"jid">>, J} | RFields]; + _ -> + [] + end; + _ -> + [] + end; + <<"">> -> + [] + end + end, Attributes). + +remove_user(_User, _Server) -> + {atomic, not_implemented}. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== init([Host, Opts]) -> State = parse_options(Host, Opts), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_VCARD, ?MODULE, process_local_iq, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, - get_sm_features, 50), eldap_pool:start_link(State#state.eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, State#state.password, State#state.tls_options), - case State#state.search of - true -> - ejabberd_router:register_route(State#state.myhost, Host); - _ -> ok - end, {ok, State}. -handle_info({route, From, To, Packet}, State) -> - case catch do_route(State, From, To, Packet) of - Pid when is_pid(Pid) -> ok; - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_INTERNAL_SERVER_ERROR), - ejabberd_router:route(To, From, Err) - end, - {noreply, State}; -handle_info(_Info, State) -> {noreply, State}. - -get_sm_features({error, _Error} = Acc, _From, _To, - _Node, _Lang) -> - Acc; -get_sm_features(Acc, _From, _To, Node, _Lang) -> - case Node of - <<"">> -> - case Acc of - {result, Features} -> {result, [?NS_VCARD | Features]}; - empty -> {result, [?NS_VCARD]} - end; - _ -> Acc - end. - -process_local_iq(_From, _To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; - get -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = - [#xmlel{name = <<"FN">>, attrs = [], - children = - [{xmlcdata, <<"ejabberd">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Erlang Jabber Server">>))/binary, - "\nCopyright (c) 2002-2016 ProcessOne">>}]}, - #xmlel{name = <<"BDAY">>, attrs = [], - children = - [{xmlcdata, <<"2002-11-16">>}]}]}]} - end. - -process_sm_iq(_From, #jid{lserver = LServer} = To, - #iq{sub_el = SubEl} = IQ) -> - case catch process_vcard_ldap(To, IQ, LServer) of - {'EXIT', _} -> - IQ#iq{type = error, - sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]}; - Other -> Other - end. - -process_vcard_ldap(To, IQ, Server) -> - {ok, State} = eldap_utils:get_state(Server, ?PROCNAME), - #iq{type = Type, sub_el = SubEl, lang = Lang} = IQ, - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; - get -> - #jid{luser = LUser} = To, - LServer = State#state.serverhost, - case ejabberd_auth:is_user_exists(LUser, LServer) of - true -> - VCardMap = State#state.vcard_map, - case find_ldap_user(LUser, State) of - #eldap_entry{attributes = Attributes} -> - Vcard = ldap_attributes_to_vcard(Attributes, VCardMap, - {LUser, LServer}), - IQ#iq{type = result, sub_el = Vcard}; - _ -> IQ#iq{type = result, sub_el = []} - end; - _ -> IQ#iq{type = result, sub_el = []} - end - end. - handle_call(get_state, _From, State) -> {reply, {ok, State}, State}; -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; handle_call(_Request, _From, State) -> - {reply, bad_request, State}. + Reply = ok, + {reply, Reply, State}. +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== find_ldap_user(User, State) -> Base = State#state.base, RFC2254_Filter = State#state.user_filter, @@ -403,309 +321,6 @@ ldap_attribute_to_vcard(vCardA, {<<"pcode">>, Value}) -> children = [{xmlcdata, Value}]}; ldap_attribute_to_vcard(_, _) -> none. --define(TLFIELD(Type, Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"type">>, Type}, - {<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). - --define(FORM(JID, SearchFields), - [#xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"You need an x:data capable client to " - "search">>)}]}, - #xmlel{name = <<"x">>, - attrs = - [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], - children = - [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search users in ">>))/binary, - (jid:to_string(JID))/binary>>}]}, - #xmlel{name = <<"instructions">>, attrs = [], - children = - [{xmlcdata, - translate:translate(Lang, - <<"Fill in fields to search for any matching " - "Jabber User">>)}]}] - ++ - lists:map(fun ({X, Y}) -> - ?TLFIELD(<<"text-single">>, X, Y) - end, - SearchFields)}]). - -do_route(State, From, To, Packet) -> - spawn(?MODULE, route, [State, From, To, Packet]). - -route(State, From, To, Packet) -> - #jid{user = User, resource = Resource} = To, - ServerHost = State#state.serverhost, - if (User /= <<"">>) or (Resource /= <<"">>) -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err); - true -> - IQ = jlib:iq_query_info(Packet), - case IQ of - #iq{type = Type, xmlns = ?NS_SEARCH, lang = Lang, - sub_el = SubEl} -> - case Type of - set -> - XDataEl = find_xdata_el(SubEl), - case XDataEl of - false -> - Txt = <<"Data form not found">>, - Err = jlib:make_error_reply( - Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), - ejabberd_router:route(To, From, Err); - _ -> - XData = jlib:parse_xdata_submit(XDataEl), - case XData of - invalid -> - Txt = <<"Incorrect data form">>, - Err = jlib:make_error_reply( - Packet, ?ERRT_BAD_REQUEST(Lang, Txt)), - ejabberd_router:route(To, From, Err); - _ -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = - [#xmlel{name = - <<"x">>, - attrs = - [{<<"xmlns">>, - ?NS_XDATA}, - {<<"type">>, - <<"result">>}], - children - = - search_result(Lang, - To, - State, - XData)}]}]}, - ejabberd_router:route(To, From, - jlib:iq_to_xml(ResIQ)) - end - end; - get -> - SearchFields = State#state.search_fields, - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_SEARCH}], - children = - ?FORM(To, SearchFields)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, xmlns = ?NS_DISCO_INFO, lang = Lang} -> - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), - ejabberd_router:route(To, From, Err); - get -> - Info = ejabberd_hooks:run_fold(disco_info, ServerHost, - [], - [ServerHost, ?MODULE, - <<"">>, <<"">>]), - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_INFO}], - children = - [#xmlel{name = - <<"identity">>, - attrs = - [{<<"category">>, - <<"directory">>}, - {<<"type">>, - <<"user">>}, - {<<"name">>, - translate:translate(Lang, - <<"vCard User Search">>)}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_SEARCH}], - children = []}, - #xmlel{name = - <<"feature">>, - attrs = - [{<<"var">>, - ?NS_VCARD}], - children = []}] - ++ Info}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = Type, lang = Lang, xmlns = ?NS_DISCO_ITEMS} -> - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - Err = jlib:make_error_reply(Packet, ?ERRT_NOT_ALLOWED(Lang, Txt)), - ejabberd_router:route(To, From, Err); - get -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, - ?NS_DISCO_ITEMS}], - children = []}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) - end; - #iq{type = get, xmlns = ?NS_VCARD, lang = Lang} -> - ResIQ = IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"vCard">>, - attrs = [{<<"xmlns">>, ?NS_VCARD}], - children = iq_get_vcard(Lang)}]}, - ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)); - _ -> - Err = jlib:make_error_reply(Packet, - ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router:route(To, From, Err) - end - end. - -iq_get_vcard(Lang) -> - [#xmlel{name = <<"FN">>, attrs = [], - children = [{xmlcdata, <<"ejabberd/mod_vcard">>}]}, - #xmlel{name = <<"URL">>, attrs = [], - children = [{xmlcdata, ?EJABBERD_URI}]}, - #xmlel{name = <<"DESC">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"ejabberd vCard module">>))/binary, - "\nCopyright (c) 2003-2016 ProcessOne">>}]}]. - --define(LFIELD(Label, Var), - #xmlel{name = <<"field">>, - attrs = - [{<<"label">>, translate:translate(Lang, Label)}, - {<<"var">>, Var}], - children = []}). - -search_result(Lang, JID, State, Data) -> - SearchReported = State#state.search_reported, - Header = [#xmlel{name = <<"title">>, attrs = [], - children = - [{xmlcdata, - <<(translate:translate(Lang, - <<"Search Results for ">>))/binary, - (jid:to_string(JID))/binary>>}]}, - #xmlel{name = <<"reported">>, attrs = [], - children = - [?TLFIELD(<<"text-single">>, <<"Jabber ID">>, - <<"jid">>)] - ++ - lists:map(fun ({Name, Value}) -> - ?TLFIELD(<<"text-single">>, Name, - Value) - end, - SearchReported)}], - case search(State, Data) of - error -> Header; - Result -> Header ++ Result - end. - --define(FIELD(Var, Val), - #xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}], - children = - [#xmlel{name = <<"value">>, attrs = [], - children = [{xmlcdata, Val}]}]}). - -search(State, Data) -> - Base = State#state.base, - SearchFilter = State#state.search_filter, - Eldap_ID = State#state.eldap_id, - UIDs = State#state.uids, - Limit = State#state.matches, - ReportedAttrs = State#state.search_reported_attrs, - Filter = eldap:'and'([SearchFilter, - eldap_utils:make_filter(Data, UIDs)]), - case eldap_pool:search(Eldap_ID, - [{base, Base}, {filter, Filter}, {limit, Limit}, - {deref_aliases, State#state.deref_aliases}, - {attributes, ReportedAttrs}]) - of - #eldap_search_result{entries = E} -> - search_items(E, State); - _ -> error - end. - -search_items(Entries, State) -> - LServer = State#state.serverhost, - SearchReported = State#state.search_reported, - VCardMap = State#state.vcard_map, - UIDs = State#state.uids, - Attributes = lists:map(fun (E) -> - #eldap_entry{attributes = Attrs} = E, Attrs - end, - Entries), - lists:flatmap(fun (Attrs) -> - case eldap_utils:find_ldap_attrs(UIDs, Attrs) of - {U, UIDAttrFormat} -> - case eldap_utils:get_user_part(U, UIDAttrFormat) - of - {ok, Username} -> - case - ejabberd_auth:is_user_exists(Username, - LServer) - of - true -> - RFields = lists:map(fun ({_, - VCardName}) -> - {VCardName, - map_vcard_attr(VCardName, - Attrs, - VCardMap, - {Username, - ?MYNAME})} - end, - SearchReported), - Result = [?FIELD(<<"jid">>, - <>)] - ++ - [?FIELD(Name, Value) - || {Name, Value} - <- RFields], - [#xmlel{name = <<"item">>, - attrs = [], - children = Result}]; - _ -> [] - end; - _ -> [] - end; - <<"">> -> [] - end - end, - Attributes). - -remove_user(_User) -> true. - -%%%----------------------- -%%% Auxiliary functions. -%%%----------------------- - map_vcard_attr(VCardName, Attributes, Pattern, UD) -> Res = lists:filter(fun ({Name, _, _}) -> eldap_utils:case_insensitive_match(Name, @@ -725,19 +340,54 @@ process_pattern(Str, {User, Domain}, AttrValues) -> [{<<"%u">>, User}, {<<"%d">>, Domain}] ++ [{<<"%s">>, V, 1} || V <- AttrValues]). -find_xdata_el(#xmlel{children = SubEls}) -> - find_xdata_el1(SubEls). +default_vcard_map() -> + [{<<"NICKNAME">>, <<"%u">>, []}, + {<<"FN">>, <<"%s">>, [<<"displayName">>]}, + {<<"FAMILY">>, <<"%s">>, [<<"sn">>]}, + {<<"GIVEN">>, <<"%s">>, [<<"givenName">>]}, + {<<"MIDDLE">>, <<"%s">>, [<<"initials">>]}, + {<<"ORGNAME">>, <<"%s">>, [<<"o">>]}, + {<<"ORGUNIT">>, <<"%s">>, [<<"ou">>]}, + {<<"CTRY">>, <<"%s">>, [<<"c">>]}, + {<<"LOCALITY">>, <<"%s">>, [<<"l">>]}, + {<<"STREET">>, <<"%s">>, [<<"street">>]}, + {<<"REGION">>, <<"%s">>, [<<"st">>]}, + {<<"PCODE">>, <<"%s">>, [<<"postalCode">>]}, + {<<"TITLE">>, <<"%s">>, [<<"title">>]}, + {<<"URL">>, <<"%s">>, [<<"labeleduri">>]}, + {<<"DESC">>, <<"%s">>, [<<"description">>]}, + {<<"TEL">>, <<"%s">>, [<<"telephoneNumber">>]}, + {<<"EMAIL">>, <<"%s">>, [<<"mail">>]}, + {<<"BDAY">>, <<"%s">>, [<<"birthDay">>]}, + {<<"ROLE">>, <<"%s">>, [<<"employeeType">>]}, + {<<"PHOTO">>, <<"%s">>, [<<"jpegPhoto">>]}]. -find_xdata_el1([]) -> false; -find_xdata_el1([#xmlel{name = Name, attrs = Attrs, - children = SubEls} - | Els]) -> - case fxml:get_attr_s(<<"xmlns">>, Attrs) of - ?NS_XDATA -> - #xmlel{name = Name, attrs = Attrs, children = SubEls}; - _ -> find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> find_xdata_el1(Els). +default_search_fields() -> + [{<<"User">>, <<"%u">>}, + {<<"Full Name">>, <<"displayName">>}, + {<<"Given Name">>, <<"givenName">>}, + {<<"Middle Name">>, <<"initials">>}, + {<<"Family Name">>, <<"sn">>}, + {<<"Nickname">>, <<"%u">>}, + {<<"Birthday">>, <<"birthDay">>}, + {<<"Country">>, <<"c">>}, + {<<"City">>, <<"l">>}, + {<<"Email">>, <<"mail">>}, + {<<"Organization Name">>, <<"o">>}, + {<<"Organization Unit">>, <<"ou">>}]. + +default_search_reported() -> + [{<<"Full Name">>, <<"FN">>}, + {<<"Given Name">>, <<"FIRST">>}, + {<<"Middle Name">>, <<"MIDDLE">>}, + {<<"Family Name">>, <<"LAST">>}, + {<<"Nickname">>, <<"NICK">>}, + {<<"Birthday">>, <<"BDAY">>}, + {<<"Country">>, <<"CTRY">>}, + {<<"City">>, <<"LOCALITY">>}, + {<<"Email">>, <<"EMAIL">>}, + {<<"Organization Name">>, <<"ORGNAME">>}, + {<<"Organization Unit">>, <<"ORGUNIT">>}]. parse_options(Host, Opts) -> MyHost = gen_mod:get_opt_host(Host, Opts, @@ -784,19 +434,19 @@ parse_options(Host, Opts) -> [iolist_to_binary(E) || E <- L]} end, Ls) - end, ?VCARD_MAP), + end, default_vcard_map()), SearchFields = gen_mod:get_opt(ldap_search_fields, Opts, fun(Ls) -> [{iolist_to_binary(S), iolist_to_binary(P)} || {S, P} <- Ls] - end, ?SEARCH_FIELDS), + end, default_search_fields()), SearchReported = gen_mod:get_opt(ldap_search_reported, Opts, fun(Ls) -> [{iolist_to_binary(S), iolist_to_binary(P)} || {S, P} <- Ls] - end, ?SEARCH_REPORTED), + end, default_search_reported()), UIDAttrs = [UAttr || {UAttr, _} <- UIDs], VCardMapAttrs = lists:usort(lists:append([A || {_, _, A} <- VCardMap]) @@ -834,27 +484,11 @@ parse_options(Host, Opts) -> search_reported_attrs = SearchReportedAttrs, matches = Matches}. -transform_module_options(Opts) -> - lists:map( - fun({ldap_vcard_map, Map}) -> - NewMap = lists:map( - fun({Field, Pattern, Attrs}) -> - {Field, [{Pattern, Attrs}]}; - (Opt) -> - Opt - end, Map), - {ldap_vcard_map, NewMap}; - (Opt) -> - Opt - end, Opts). - check_filter(F) -> NewF = iolist_to_binary(F), {ok, _} = eldap_filter:parse(NewF), NewF. -mod_opt_type(host) -> fun iolist_to_binary/1; -mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; mod_opt_type(ldap_filter) -> fun check_filter/1; mod_opt_type(ldap_search_fields) -> fun (Ls) -> @@ -882,12 +516,6 @@ mod_opt_type(ldap_vcard_map) -> end, Ls) end; -mod_opt_type(matches) -> - fun (infinity) -> 0; - (I) when is_integer(I), I > 0 -> I - end; -mod_opt_type(search) -> - fun (B) when is_boolean(B) -> B end; mod_opt_type(deref_aliases) -> fun (never) -> never; (searching) -> searching; @@ -926,9 +554,9 @@ mod_opt_type(ldap_tls_verify) -> (false) -> false end; mod_opt_type(_) -> - [host, iqdisc, ldap_filter, ldap_search_fields, + [ldap_filter, ldap_search_fields, ldap_search_reported, ldap_uids, ldap_vcard_map, - matches, search, deref_aliases, ldap_backups, ldap_base, + deref_aliases, ldap_backups, ldap_base, ldap_deref_aliases, ldap_encrypt, ldap_password, ldap_port, ldap_rootdn, ldap_servers, ldap_tls_cacertfile, ldap_tls_certfile, ldap_tls_depth, diff --git a/src/mod_vcard_mnesia.erl b/src/mod_vcard_mnesia.erl index 67abed09b..3b64c29ef 100644 --- a/src/mod_vcard_mnesia.erl +++ b/src/mod_vcard_mnesia.erl @@ -11,7 +11,8 @@ -behaviour(mod_vcard). %% API --export([init/2, import/2, get_vcard/2, set_vcard/4, search/4, remove_user/2]). +-export([init/2, stop/1, import/2, get_vcard/2, set_vcard/4, search/4, + search_fields/1, search_reported/1, remove_user/2]). -include("ejabberd.hrl"). -include("xmpp.hrl"). @@ -43,6 +44,9 @@ init(_Host, _Opts) -> mnesia:add_table_index(vcard_search, lorgname), mnesia:add_table_index(vcard_search, lorgunit). +stop(_Host) -> + ok. + get_vcard(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> mnesia:read({vcard, US}) end, @@ -71,15 +75,44 @@ search(LServer, Data, AllowReturnAll, MaxMatch) -> {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]), []; Rs -> + Fields = lists:map(fun record_to_item/1, Rs), case MaxMatch of infinity -> - Rs; + Fields; Val -> - lists:sublist(Rs, Val) + lists:sublist(Fields, Val) end end end. +search_fields(_LServer) -> + [{<<"User">>, <<"user">>}, + {<<"Full Name">>, <<"fn">>}, + {<<"Name">>, <<"first">>}, + {<<"Middle Name">>, <<"middle">>}, + {<<"Family Name">>, <<"last">>}, + {<<"Nickname">>, <<"nick">>}, + {<<"Birthday">>, <<"bday">>}, + {<<"Country">>, <<"ctry">>}, + {<<"City">>, <<"locality">>}, + {<<"Email">>, <<"email">>}, + {<<"Organization Name">>, <<"orgname">>}, + {<<"Organization Unit">>, <<"orgunit">>}]. + +search_reported(_LServer) -> + [{<<"Jabber ID">>, <<"jid">>}, + {<<"Full Name">>, <<"fn">>}, + {<<"Name">>, <<"first">>}, + {<<"Middle Name">>, <<"middle">>}, + {<<"Family Name">>, <<"last">>}, + {<<"Nickname">>, <<"nick">>}, + {<<"Birthday">>, <<"bday">>}, + {<<"Country">>, <<"ctry">>}, + {<<"City">>, <<"locality">>}, + {<<"Email">>, <<"email">>}, + {<<"Organization Name">>, <<"orgname">>}, + {<<"Organization Unit">>, <<"orgunit">>}]. + remove_user(LUser, LServer) -> US = {LUser, LServer}, F = fun () -> @@ -211,3 +244,19 @@ parts_to_string(Parts) -> str:strip(list_to_binary( lists:map(fun (S) -> <> end, Parts)), right, $.). + +-spec record_to_item(#vcard_search{}) -> [{binary(), binary()}]. +record_to_item(R) -> + {User, Server} = R#vcard_search.user, + [{<<"jid">>, <>}, + {<<"fn">>, (R#vcard_search.fn)}, + {<<"last">>, (R#vcard_search.family)}, + {<<"first">>, (R#vcard_search.given)}, + {<<"middle">>, (R#vcard_search.middle)}, + {<<"nick">>, (R#vcard_search.nickname)}, + {<<"bday">>, (R#vcard_search.bday)}, + {<<"ctry">>, (R#vcard_search.ctry)}, + {<<"locality">>, (R#vcard_search.locality)}, + {<<"email">>, (R#vcard_search.email)}, + {<<"orgname">>, (R#vcard_search.orgname)}, + {<<"orgunit">>, (R#vcard_search.orgunit)}]. diff --git a/src/mod_vcard_riak.erl b/src/mod_vcard_riak.erl index 397008a79..23f05f17d 100644 --- a/src/mod_vcard_riak.erl +++ b/src/mod_vcard_riak.erl @@ -12,7 +12,7 @@ %% API -export([init/2, get_vcard/2, set_vcard/4, search/4, remove_user/2, - import/2]). + search_fields/1, search_reported/1, import/2, stop/1]). -include("xmpp.hrl"). -include("mod_vcard.hrl"). @@ -23,6 +23,9 @@ init(_Host, _Opts) -> ok. +stop(_Host) -> + ok. + get_vcard(LUser, LServer) -> case ejabberd_riak:get(vcard, vcard_schema(), {LUser, LServer}) of {ok, R} -> @@ -89,6 +92,12 @@ set_vcard(LUser, LServer, VCARD, search(_LServer, _Data, _AllowReturnAll, _MaxMatch) -> []. +search_fields(_LServer) -> + []. + +search_reported(_LServer) -> + []. + remove_user(LUser, LServer) -> {atomic, ejabberd_riak:delete(vcard, {LUser, LServer})}. diff --git a/src/mod_vcard_sql.erl b/src/mod_vcard_sql.erl index f448d0776..f8ac6be97 100644 --- a/src/mod_vcard_sql.erl +++ b/src/mod_vcard_sql.erl @@ -13,8 +13,8 @@ -behaviour(mod_vcard). %% API --export([init/2, get_vcard/2, set_vcard/4, search/4, remove_user/2, - import/1, import/2, export/1]). +-export([init/2, stop/1, get_vcard/2, set_vcard/4, search/4, remove_user/2, + search_fields/1, search_reported/1, import/1, import/2, export/1]). -include("xmpp.hrl"). -include("mod_vcard.hrl"). @@ -27,6 +27,9 @@ init(_Host, _Opts) -> ok. +stop(_Host) -> + ok. + get_vcard(LUser, LServer) -> case catch sql_queries:get_vcard(LServer, LUser) of {selected, [{SVCARD}]} -> @@ -93,12 +96,40 @@ search(LServer, Data, AllowReturnAll, MaxMatch) -> <<"middle">>, <<"nickname">>, <<"bday">>, <<"ctry">>, <<"locality">>, <<"email">>, <<"orgname">>, <<"orgunit">>], Rs} when is_list(Rs) -> - Rs; + [row_to_item(LServer, R) || R <- Rs]; Error -> ?ERROR_MSG("~p", [Error]), [] end end. +search_fields(_LServer) -> + [{<<"User">>, <<"user">>}, + {<<"Full Name">>, <<"fn">>}, + {<<"Name">>, <<"first">>}, + {<<"Middle Name">>, <<"middle">>}, + {<<"Family Name">>, <<"last">>}, + {<<"Nickname">>, <<"nick">>}, + {<<"Birthday">>, <<"bday">>}, + {<<"Country">>, <<"ctry">>}, + {<<"City">>, <<"locality">>}, + {<<"Email">>, <<"email">>}, + {<<"Organization Name">>, <<"orgname">>}, + {<<"Organization Unit">>, <<"orgunit">>}]. + +search_reported(_LServer) -> + [{<<"Jabber ID">>, <<"jid">>}, + {<<"Full Name">>, <<"fn">>}, + {<<"Name">>, <<"first">>}, + {<<"Middle Name">>, <<"middle">>}, + {<<"Family Name">>, <<"last">>}, + {<<"Nickname">>, <<"nick">>}, + {<<"Birthday">>, <<"bday">>}, + {<<"Country">>, <<"ctry">>}, + {<<"City">>, <<"locality">>}, + {<<"Email">>, <<"email">>}, + {<<"Organization Name">>, <<"orgname">>}, + {<<"Organization Unit">>, <<"orgunit">>}]. + remove_user(LUser, LServer) -> ejabberd_sql:sql_transaction( LServer, @@ -240,3 +271,18 @@ make_val(Match, Field, Val) -> <<"">> -> Condition; _ -> [Match, <<" and ">>, Condition] end. + +row_to_item(LServer, [Username, FN, Family, Given, Middle, Nickname, BDay, + CTRY, Locality, EMail, OrgName, OrgUnit]) -> + [{<<"jid">>, <>}, + {<<"fn">>, FN}, + {<<"last">>, Family}, + {<<"first">>, Given}, + {<<"middle">>, Middle}, + {<<"nick">>, Nickname}, + {<<"bday">>, BDay}, + {<<"ctry">>, CTRY}, + {<<"locality">>, Locality}, + {<<"email">>, EMail}, + {<<"orgname">>, OrgName}, + {<<"orgunit">>, OrgUnit}].