Projects
Kolab:16:Testing
guam
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 46
View file
guam.spec
Changed
@@ -18,7 +18,7 @@ %define lock_version() %{1}%{?_isa} = %(rpm -q --queryformat "%%{VERSION}" %{1}) Name: guam -Version: 0.9.9 +Version: 0.9.10 Release: 1%{?dist} Summary: A Smart Reverse IMAP Proxy @@ -224,6 +224,9 @@ %endif %changelog +* Mon May 31 2021 Christian Mollekopf <mollekopf@kolabsys.com> - 0.9.10-1 +- Release of version 0.9.10 + * Mon Dec 14 2020 Christian Mollekopf <mollekopf@kolabsys.com> - 0.9.9-1 - Release of version 0.9.9
View file
guam-0.9.4-D714-for-T160184.patch
Deleted
@@ -1,132 +0,0 @@ -diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl -index decbd9a..d0e7ca1 100644 ---- a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl -+++ b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl -@@ -102,6 +102,7 @@ filter_folder(_State, <<>>, Acc) -> { Acc, true }; - filter_folder(State, <<"* LIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; - filter_folder(State, <<"* XLIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; - filter_folder(State, <<"* LSUB ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; -+filter_folder(State, <<"* STATUS ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; - filter_folder(#state{ tag = Tag }, Response, Acc) -> - HasMore = - case byte_size(Tag) =< byte_size(Response) of -@@ -115,25 +116,28 @@ filter_folder(#state{ tag = Tag }, Response, Acc) -> - { add_response(Response, Acc), HasMore }. - - filter_on_details(#state{ blacklist = Blacklist }, Response, Acc, Details) -> -- %% first determine if we have a quoted item or a non-quoted item and start from there -- DetailsSize = byte_size(Details), -- { Quoted, Start } = case binary:at(Details, DetailsSize - 1) of $" -> { quoted, DetailsSize - 2 }; _ -> { unquoted, DetailsSize - 1 } end, -- Folder = find_folder_name(Details, Quoted, Start, Start, binary:at(Details, Start)), -- %io:format("COMPARING ~p ??? ~p~n", Folder, in_blacklist(Folder, Blacklist)), -+ %% Remove "*" and extract response command name -+ { _, Start, _ } = pop_token(Response), %% asterisk -+ { Cmd, _, _ } = pop_token(Start), %% command -+ -+ %% Extract folder name -+ Suffix = -+ case Cmd =:= <<"STATUS">> of -+ true -> Details; -+ _ -> -+ { Pos, _Length } = binary:match(Details, <<")">>, ), -+ { _Delimiter, Rest, _} = pop_token(binary:part(Details, Pos + 2, byte_size(Details) - Pos - 2)), -+ Rest -+ end, -+ { Folder, _, _ } = pop_token(list_to_binary(Suffix, <<"\r\n">>)), -+ -+ %% Check the folder in blacklist -+ %% io:format("COMPARING ~p ??? ~p~n", Folder, in_blacklist(Folder, Blacklist)), - case in_blacklist(Folder, Blacklist) of - true -> Acc; - _ -> add_response(Response, Acc) - end. - --find_folder_name(Details, quoted, End, Start, $") -> -- binary:part(Details, Start + 1, End - Start); --find_folder_name(Details, unquoted, End, Start, $ ) -> -- binary:part(Details, Start + 1, End - Start); --find_folder_name(Details, _Quoted, _End, 0, _) -> -- Details; --find_folder_name(Details, Quoted, End, Start, _) -> -- find_folder_name(Details, Quoted, End, Start - 1, binary:at(Details, Start - 1)). -- - add_response(Response, <<>>) -> Response; - add_response(Response, Acc) -> <<Acc/binary, "\r\n", Response/binary>>. - -@@ -147,3 +151,76 @@ in_blacklist(Folder, { Literal, Prefix }|List) -> - _ -> in_blacklist(Folder, List) - end - end. -+ -+%% pop_token from https://github.com/MainframeHQ/switchboard/blob/master/src/imap.erl (BSD Lic.) -+%% with some small changes by Aleksander Machniak <machniak@kolabsys.com> -+pop_token(Data) -> -+ pop_token(Data, none). -+ -+pop_token(<<>>, State) -> -+ {none, <<>>, State}; -+ -+%% Consume hanging spaces -+pop_token(<<" ", Rest/binary>>, none) -> -+ pop_token(Rest, none); -+ -+%% \r\n -+pop_token(<<$\r, $\n, Rest/binary>>, none) -> -+ {crlf, Rest, none}; -+ -+%% NIL -+pop_token(<<"NIL", Rest/binary>>, none) -> -+ {nil, Rest, none}; -+ -+%% ( | ) | | -+pop_token(<<$(, Rest/binary>>, none) -> -+ {'(', Rest, none}; -+pop_token(<<$), Rest/binary>>, none) -> -+ {')', Rest, none}; -+ -+pop_token(<<$, Rest/binary>>, none) -> -+ {'', Rest, none}; -+pop_token(<<$, Rest/binary>>, none) -> -+ {'', Rest, none}; -+ -+%% Atom -+pop_token(<<C, _/binary>> = Data, {atom, AtomAcc}) when -+ C =:= 32; C =:= 40; C =:= 41; C =:= $(; C =:= $); C =:= 91; C =:= 93 -> -+ {AtomAcc, Data, none}; -+pop_token(<<$\r, $\n, _/binary>> = Data, {atom, AtomAcc}) -> -+ {AtomAcc, Data, none}; -+pop_token(<<C, Rest/binary>>, none) when C >= 35, C < 123 -> -+ pop_token(Rest, {atom, <<C>>}); -+pop_token(<<C, Rest/binary>>, {atom, AtomAcc}) when C >= 35, C < 123 -> -+ pop_token(Rest, {atom, <<AtomAcc/binary, C>>}); -+ -+%% Literal Strings -+pop_token(<<${, Rest/binary>>, none) -> -+ pop_token(Rest, {literal, <<>>}); -+pop_token(<<$}, $\r, $\n, Rest/binary>>, {literal, ByteAcc}) -> -+ pop_token(Rest, {literal, binary_to_integer(ByteAcc), <<>>}); -+pop_token(<<D, Rest/binary>>, {literal, ByteAcc}) when D >= 48, D < 58 -> -+ pop_token(Rest, {literal, <<ByteAcc/binary, D>>}); -+pop_token(Binary, {literal, Bytes, LiteralAcc}) when is_integer(Bytes) -> -+ case Binary of -+ <<Literal:Bytes/binary, Rest/binary>> -> -+ {<<LiteralAcc/binary, Literal/binary>>, Rest, none}; -+ _ -> -+ %% If the binary is too short, accumulate it in the state -+ pop_token(<<>>, {literal, Bytes - size(Binary), <<LiteralAcc/binary, Binary/binary>>}) -+ end; -+ -+%% Quoted Strings -+pop_token(<<$", Rest/binary>>, none) -> -+ pop_token(Rest, {quoted, <<>>}); -+pop_token(<<$\\, C, Rest/binary>>, {quoted, Acc}) -> -+ pop_token(Rest, {quoted, <<Acc/binary, C>>}); -+pop_token(<<$", Rest/binary>>, {quoted, Acc}) -> -+ {Acc, Rest, none}; -+pop_token(<<$\r, $\n, _>>, {quoted, _}) -> -+ throw({error, crlf_in_quoted}); -+pop_token(<<C, Rest/binary>>, {quoted, Acc}) -> -+ pop_token(Rest, {quoted, <<Acc/binary, C>>}); -+ -+pop_token(Binary, _) -> -+ {none, Binary, none}.
View file
debian.changelog
Changed
@@ -1,3 +1,9 @@ +guam (0.9.10-1) unstable; urgency=medium + + * Release of version 0.9.10 + + -- Christian Mollekopf (Kolab Systems) <mollekopf@kolabsys.com> Mon, 31 May 2021 09:31:31 +0200 + guam (0.9.9-1) unstable; urgency=medium * Release of version 0.9.9
View file
guam-0.9.9.tar.gz/CHANGELOG.md -> guam-0.9.10.tar.gz/CHANGELOG.md
Changed
@@ -11,6 +11,10 @@ ### Fixed ### Security +## 0.9.10 - 2021-05-31 +### Changed +- Added an audit rule to log login attempts. + ## 0.9.9 - 2020-12-14 ### Changed - Set LimitNOFILE=1048576 in the guam.service file
View file
guam-0.9.9.tar.gz/app.config -> guam-0.9.10.tar.gz/app.config
Changed
@@ -21,6 +21,18 @@ { port, 143 }, { tls, starttls } + }, + { kolabnow, + { host, "imap.kolabnow.com" }, + { port, 993 }, + { tls, true } + + }, + { localhost, + { host, "127.0.0.1" }, + { port, 993 }, + { tls, true } + } }, @@ -56,6 +68,27 @@ }, { tls_config, { certfile, "/etc/ssl/sample.cert" }, { keyfile, "/etc/ssl/sample.key" } } + }, + { localhost, + { port, 1995 }, + { imap_server, localhost }, + { rules, + { filter_groupware, } + + } + + }, + { kolabnow, + { port, 1996 }, + { imap_server, kolabnow }, + + { implicit_tls, false }, + { rules, + { filter_groupware, }, + { audit, } + + } + } } @@ -73,3 +106,4 @@ } . +%%{lager_syslog_backend, "guam", mail, info},
View file
guam-0.9.9.tar.gz/apps/kolab_guam/src/kolab_guam.app.src -> guam-0.9.10.tar.gz/apps/kolab_guam/src/kolab_guam.app.src
Changed
@@ -2,7 +2,7 @@ {application, kolab_guam, {description, "IMAP session proxy"}, - {vsn, "0.9.9"}, + {vsn, "0.9.10"}, {registered, }, {applications, kernel,
View file
guam-0.9.9.tar.gz/apps/kolab_guam/src/kolab_guam_rule.erl -> guam-0.9.10.tar.gz/apps/kolab_guam/src/kolab_guam_rule.erl
Changed
@@ -20,16 +20,20 @@ -callback new(Args :: any()) -> State :: any(). +%% Used to decied if a rule applies to a session (is active). No longer called once the rule is decided (true/false). -callback applies(ConnectionDetails :: list(), Buffer :: binary(), SplitBinary :: { Tag :: binary(), Command :: binary(), Data :: binary() }, State :: any()) -> { true, State :: any() } | { false, State :: any() } | { notyet, State :: any() }. +%% Called for rules in the active set. -callback apply_to_client_message(ImapSession :: pid(), Command :: binary(), SplitBinary :: { Tag :: binary(), Command :: binary(), Data :: binary() }, State :: any()) -> { ProcessedCommand :: binary(), State :: any() }. +%% Called for rules in the active set. -callback apply_to_server_message(ImapSession :: pid(), Command :: binary(), State :: any()) -> { ProcessedCommand :: binary(), State :: any() }. +%% Callback for rules that request extra data from the server -callback imap_data(ResponseToken :: any(), Response :: any(), State :: any()) -> State :: any().
View file
guam-0.9.10.tar.gz/apps/kolab_guam/src/rules/kolab_guam_rule_audit.erl
Added
@@ -0,0 +1,93 @@ +%% Copyright 2021 Apheleia IT AG (http://www.apheleia.ch) +%% +%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com> +%% Christian Mollekopf (Apheleia IT) <mollekopf a apheleia.ch> +%% +%% 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 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 General Public License for more details. +%% +%% You should have received a copy of the GNU General Public License +%% along with this program. If not, see <http://www.gnu.org/licenses/>. + +-module(kolab_guam_rule_audit). +-export(new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3). +-behavior(kolab_guam_rule). + +-include("kolab_guam_rule_audit.hrl"). + +new(_Config) -> #state { }. + +applies(Socket, _Buffer, { _Tag, _Command, _Data }, State) -> + {ok, {Ip, _Port}} = inet:peername(Socket), + % This command is always immediately active as we expect the LOGIN command at the beginning + { true, State#state{ ip = Ip }}. + + +% Unused +imap_data(_ResponseToken, _Response, State) -> State. + + +apply_to_client_message(_ImapSession, Buffer, undefined, State) -> + { Buffer, State }; + +% We just buffer the entire command once activated +apply_to_client_message(_ImapSession, Buffer, { _Tag, _Command, _Data }, #state{ buffer = LeftOvers, active = true } = State) -> + { Buffer, State#state{ buffer = <<LeftOvers/binary, Buffer/binary>> }}; + +% Monitor for the trigger command, otherwise do nothing +apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) -> + case is_triggering_command(Command, Data, State) of + true -> apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State#state{ active = true, tag = Tag, command = Command }); + _ -> { Buffer, State } + end. + + +apply_to_server_message(_ImapSession, Buffer, #state{ active = true, tag = Tag, ip = Ip, buffer = FullBuffer, command = Command } = State) -> + NewState = case eimap_utils:is_tagged_response(Buffer, Tag) of + tagged -> + Username = extract_username(FullBuffer, Command), + case eimap_utils:check_response_for_failure(Buffer, Tag) of + ok -> + lager:info("LOGIN ATTEMPT: ~p from ~p, OK", Username, inet:ntoa(Ip)), + State#state{ active = false, username = Username }; + { no, Reason } -> + lager:info("LOGIN ATTEMPT: ~p from ~p, NO: ~p", Username, inet:ntoa(Ip), Reason), + State#state{ active = false, username = Username }; + { bad, Reason } -> + lager:info("LOGIN ATTEMPT: ~p from ~p, BAD: ~p", Username, inet:ntoa(Ip), Reason), + State#state{ active = false, username = Username } + end; + untagged -> State + end, + { Buffer, NewState }; + +% Do nothing if not active +apply_to_server_message(_ImapSession, Buffer, State) -> + { Buffer, State }. + +%%PRIVATE +is_triggering_command(Command, Data, #state{ trigger_commands = TriggerCommands }) -> + %% if the command is in the list of trigger commands and the ending is not "" (which means "send me + %% the root and separator" according to RFC 3501), then it is treated as a triggering event + lists:any(fun(T) -> (Command =:= T) andalso (binary:longest_common_suffix(Data, <<"\"\"">>) =/= 2) end, + TriggerCommands). + + +extract_username(FullBuffer, Command) -> + case Command of + Command when Command =:= <<"AUTHENTICATE">>; Command =:= <<"authenticate">> -> + Lines = binary:split(FullBuffer, <<"\r\n">>, global ), + % We can only handle the LOGIN method + lager:info("Lines ~p", Lines), + base64:decode(lists:nth(2, Lines)); + <<"LOGIN">> -> + List = binary:split(FullBuffer, <<" ">>, global ), + lists:nth(3, List) + end.
View file
guam-0.9.10.tar.gz/apps/kolab_guam/src/rules/kolab_guam_rule_audit.hrl
Added
@@ -0,0 +1,3 @@ +-record(state, { tag = <<>>, active = false, buffer = <<>>, ip = "", username = <<>>, command = <<>>, + trigger_commands = <<"LOGIN">>, <<"login">>, <<"AUTHENTICATE">>, <<"authenticate">>}). +
View file
guam-0.9.9.tar.gz/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl -> guam-0.9.10.tar.gz/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl
Changed
@@ -44,8 +44,8 @@ filter_folders(Buffer, State); apply_to_server_message(_ImapSession, Buffer, State) -> { Buffer, State }. -imap_data(blacklist, { error, _Reason }, State) -> State; -imap_data(blacklist, Response, State) -> +imap_data(_ResponseToken, { error, _Reason }, State) -> State; +imap_data(_ResponseToken, Response, State) -> %TODO: we don't need Foo/Bar if we already have Foo, so filter folders-of-groupwarefolders Blacklist = lists:foldl(fun({ _Folder, { _Property, null } }, Acc) -> Acc; ({ _Folder, { _Property, <<"mail", _Rest/binary>> } }, Acc) -> Acc;
View file
guam-0.9.10.tar.gz/apps/kolab_guam/test/kolab_guam_rule_audit_SUITE.erl
Added
@@ -0,0 +1,111 @@ +%% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) +%% +%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com> +%% +%% 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 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 General Public License for more details. +%% +%% You should have received a copy of the GNU General Public License +%% along with this program. If not, see <http://www.gnu.org/licenses/>. + +-module(kolab_guam_rule_audit_SUITE). + +% easier than exporting by name +-compile(export_all). + +% required for common_test to work +-include_lib("common_test/include/ct.hrl"). +-include("../src/rules/kolab_guam_rule_audit.hrl"). + +%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% common test callbacks %% +%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% Specify a list of all unit test functions +all() -> + kolab_guam_rule_audit_test + . + +% required, but can just return Config. this is a suite level setup function. +init_per_suite(Config) -> + Config. + +% required, but can just return Config. this is a suite level tear down function. +end_per_suite(Config) -> + Config. + +% optional, can do function level setup for all functions, +% or for individual functions by matching on TestCase. +init_per_testcase(_TestCase, Config) -> + Config. + +% optional, can do function level tear down for all functions, +% or for individual functions by matching on TestCase. +end_per_testcase(_TestCase, Config) -> + Config. + +kolab_guam_rule_audit_test(_TestConfig) -> + %% Data to be fed into the test, one tuple per iteration + %% Tuple format: { client_data, server_data, LoginAttemptSucceeded } + Data = + { + <<"a001 LOGIN test1@kolab.org SESAME\r\n">>, + <<"a001 OK LOGIN completed">>, + true + }, + { + <<"a001 LOGIN test1@kolab.org SESAME\r\n">>, + <<"a001 NO LOGIN completed">>, + false + }, + { + <<"a001 LOGIN test1@kolab.org SESAME\r\n">>, + <<"a001 BAD LOGIN completed">>, + false + }, + { + <<"a001 AUTHENTICATE LOGIN\r\n">>, + <<"dGVzdDFAa29sYWIub3Jn\r\n">>, + <<"V2VsY29tZTJLb2xhYlN5c3RlbXM=\r\n">>, + <<"a001 BAD LOGIN completed">>, + false + }, + { + <<"a001 AUTHENTICATE LOGIN\r\n">>, + <<"dGVzdDFAa29sYWIub3Jn\r\n">>, + <<"V2VsY29tZTJLb2xhYlN5c3RlbXM=\r\n">>, + <<"a001 OK LOGIN completed">>, + true + } + , + + lager:start(), + lager:set_loglevel(lager_console_backend, debug), + + %% setup boilerplate + Config = {}, + State = kolab_guam_rule_audit:new(Config), + ServerConfig = kolab_guam_sup:default_imap_server_config(), + { ok, ImapSession } = eimap:start_link(ServerConfig), + + %% run the dataset through the rule + lists:foreach( + fun({ ClientDataList, ServerData, LoginAttemptSucceeded }) -> + ReadyState = lists:foldl(fun(ClientData, State) -> + Split = eimap_utils:split_command_into_components(ClientData), + { _, ReadyState } = kolab_guam_rule_audit:apply_to_client_message(ImapSession, ClientData, Split, State), + ReadyState + end, State, ClientDataList), + { _Filtered, NewState } = kolab_guam_rule_audit:apply_to_server_message(ImapSession, ServerData, ReadyState), + lager:info("Result ~p", NewState), + #state{ username = <<"test1@kolab.org">> } = NewState + % true = false + end, + Data).
View file
guam-0.9.9.tar.gz/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl -> guam-0.9.10.tar.gz/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl
Changed
@@ -76,7 +76,7 @@ , %% setup boilerplate - Config = {}, %%TODO? + Config = {}, State = kolab_guam_rule_filter_groupware:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), @@ -176,7 +176,8 @@ <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* STATUS \"INBOX\" (MESSAGES 17 UNSEEN 16)\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> }, %String literals - %Filtering on string literals is broken, it will attempt to filter on {8}Calendar + %FIXME Filtering on string literals is broken, it will attempt to filter on {8}Calendar + %It doesn't currently matter though, because there's no literals in folder names (not normally anyways) { %{<<"Calendar">>, <<"Calendar/">>} @@ -190,7 +191,7 @@ } , %% setup boilerplate - Config = {}, %%TODO? + Config = {}, State = kolab_guam_rule_filter_groupware:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), @@ -211,3 +212,6 @@ { Processed, State } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, ReadyState), filter_groupware_packets(ImapSession, State, More, <<Buffer/binary, Processed/binary>>). + + +
View file
guam-0.9.9.tar.gz/apps/kolab_guam/test/test.config -> guam-0.9.10.tar.gz/apps/kolab_guam/test/test.config
Changed
@@ -29,7 +29,8 @@ { handlers, - { lager_console_backend, info }, + { lager_console_backend, debug }, + { lager_common_test_backend, debug }, { lager_file_backend, { file, "log/error.log"}, { level, error } }, { lager_file_backend, { file, "log/console.log"}, { level, debug } } }
View file
guam-0.9.9.tar.gz/rebar.config -> guam-0.9.10.tar.gz/rebar.config
Changed
@@ -23,7 +23,7 @@ { eunit_opts, verbose, {skip_deps, true } }. { eunit_exclude_deps, true }. { cover_enabled, true }. -{ relx, { release, { guam, "0.9.9" }, kolab_guam}, +{ relx, { release, { guam, "0.9.10" }, kolab_guam}, { dev_mode, false }, { include_erts, false }, { extended_start_script, true },
View file
guam-0.9.9.tar.gz/test.spec.in -> guam-0.9.10.tar.gz/test.spec.in
Changed
@@ -1,4 +1,4 @@ {cover, "cover.spec"}. {logdir, "@PATH@/test_logs"}. {alias, kolab_guam, "@PATH@/apps/kolab_guam"}. -{suites, kolab_guam, kolab_guam_sup_SUITE, kolab_guam_rules_SUITE}. +{suites, kolab_guam, kolab_guam_sup_SUITE, kolab_guam_rules_SUITE, kolab_guam_rule_audit_SUITE}.
View file
guam.dsc
Changed
@@ -2,7 +2,7 @@ Source: guam Binary: guam Architecture: any -Version: 0.9.9-1 +Version: 0.9.10-1 Maintainer: Christoph Erhardt <kolab@sicherha.de> Homepage: https://kolab.org/about/guam Standards-Version: 3.9.6 @@ -10,5 +10,5 @@ Package-List: guam deb mail extra Files: - 00000000000000000000000000000000 0 guam-0.9.9.tar.gz + 00000000000000000000000000000000 0 guam-0.9.10.tar.gz 00000000000000000000000000000000 0 debian.tar.gz
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.