Projects
Kolab:16
kolab-syncroton
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 31
View file
kolab-syncroton.spec
Changed
@@ -36,8 +36,8 @@ %global _ap_sysconfdir %{_sysconfdir}/%{httpd_name} Name: kolab-syncroton -Version: 2.3.15 -Release: 2%{?dist} +Version: 2.3.16 +Release: 1%{?dist} Summary: ActiveSync for Kolab Groupware Group: Applications/Internet @@ -50,8 +50,6 @@ Patch0: defaults.patch -Patch0001: 0001-Accept-MeetingResponse-commands-from-within-the-iOS-.patch - BuildArch: noarch # Use this build requirement to make sure we are using @@ -101,8 +99,6 @@ %patch0 -p1 -%patch0001 -p1 - %build %install @@ -211,6 +207,9 @@ %attr(0770,%{httpd_user},%{httpd_group}) %{_var}/log/%{name} %changelog +* Wed Dec 4 2019 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 2.3.16-1 +- Release version 2.3.16 + * Mon Jul 29 2019 Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> - 2.3.15-3 - Fix MeetingResponse for Calendar events
View file
0001-Accept-MeetingResponse-commands-from-within-the-iOS-.patch
Deleted
@@ -1,91 +0,0 @@ -From 882701777baf1991e5c33bf55b5b1985794fa67b Mon Sep 17 00:00:00 2001 -From: Jeroen van Meeuwen <kanarip@kanarip.ch> -Date: Mon, 29 Jul 2019 11:57:09 +0200 -Subject: [PATCH] Accept MeetingResponse commands from within the iOS calendar - application. - -Summary: Resolves Bifrost#T228115 - -Test Plan: -* Configure an ActiveSync account on an iOS device. - -* Create an invitation from another user with the current user as a participant. - -* Synchronize the iOS device. - -* Click or edit the event in the calendar to change from/to Accepted/Tentative (Maybe)/Decline. - -* Refresh web interface and view event details. - -* As the organizer, expect a response. - -Reviewers: #syncroton_developers - -Subscribers: #syncroton_developers - -Differential Revision: https://git.kolab.org/D773 ---- - lib/kolab_sync_data_calendar.php | 26 ++++++++++++++++++++++---- - 1 file changed, 22 insertions(+), 4 deletions(-) - -diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php -index e822c73..010dc23 100644 ---- a/lib/kolab_sync_data_calendar.php -+++ b/lib/kolab_sync_data_calendar.php -@@ -596,7 +596,11 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - $event = $this->get_event_from_invitation($request); - - // find the event in calendar -- $existing = $this->find_event_by_uid($event['uid']); -+ if (array_key_exists('uid', $event)) { -+ $existing = $this->find_event_by_uid($event['uid']); -+ } else { -+ $existing = $this->find_event_by_uid($event->uID); -+ } - /* - switch ($status) { - case 'ACCEPTED': $event['free_busy'] = 'busy'; break; -@@ -665,7 +669,11 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - // as it's expected by the specification. Server - // should delete an event in such a case, but we - // keep the event copy with appropriate attendee status instead. -- return empty($status) ? null : $this->serverId($event['uid'], $folder); -+ return empty($status) ? null : ( -+ array_key_exists('uid', $event) ? -+ $this->serverId($event['uid'], $folder) : -+ $this->serverId($event->uID, $folder) -+ ); - } - - /** -@@ -684,6 +692,14 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - return $event; - } - -+ $collection = new Syncroton_Model_SyncCollection( -+ array('collectionId' => $request->collectionId) -+ ); -+ -+ if ($event = $this->getEntry($collection, $request->requestId)) { -+ return $event; -+ } -+ - throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); - } - -@@ -723,8 +739,10 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data - - $this->update_attendee_status($old, $status); - -- if ($event['free_busy']) { -- $old['free_busy'] = $event['free_busy']; -+ if (array_key_exists('free_busy', $event)) { -+ if ($event['free_busy']) { -+ $old['free_busy'] = $event['free_busy']; -+ } - } - - // Updating an existing event is most-likely a response --- -2.20.1 -
View file
debian.changelog
Changed
@@ -1,3 +1,9 @@ +kolab-syncroton (2.3.16-0~kolab1) unstable; urgency=low + + * Release version 2.3.16 + + -- Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> Wed, 4 Dec 2019 15:13:40 +0200 + kolab-syncroton (2.3.15-0~kolab6) unstable; urgency=low * Fix MeetingResponse parsing for Calendar events
View file
debian.series
Changed
@@ -1,2 +1,1 @@ defaults.patch -p1 -0001-Accept-MeetingResponse-commands-from-within-the-iOS-.patch -p1
View file
kolab-syncroton-2.3.15.tar.gz/config/config.inc.php.dist -> kolab-syncroton-2.3.16.tar.gz/config/config.inc.php.dist
Changed
@@ -67,7 +67,10 @@ // List of Roundcube plugins // WARNING: Not all plugins used in Roundcube can be listed here -$config['activesync_plugins'] = array(); +$config['activesync_plugins'] = array( + 'libcalendaring', + 'libkolab' +); // Defines for how many seconds we'll sleep between every // action for detecting changes in folders. Default: 60 @@ -97,13 +100,13 @@ // 8 - all folders in other users namespace // 16 - all subscribed folders in shared namespace // 32 - all folders in shared namespace -$config['activesync_init_subscriptions'] = 0; +$config['activesync_init_subscriptions'] = 21; // Defines blacklist of devices (device type strings) that do not support folder hierarchies. // When set to an array folder hierarchies are used on all devices not listed here. // When set to null an old whitelist approach will be used where we do opposite // action and enable folder hierarchies only on device types known to support it. -$config['activesync_multifolder_blacklist'] = null; +$config['activesync_multifolder_blacklist'] = array(); // Blacklist overwrites for specified object type. If set to an array // it will have a precedence over 'activesync_multifolder_blacklist' list only for that type. @@ -111,7 +114,7 @@ // in that case use $config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook'); $config['activesync_multifolder_blacklist_mail'] = null; $config['activesync_multifolder_blacklist_event'] = null; -$config['activesync_multifolder_blacklist_contact'] = null; +$config['activesync_multifolder_blacklist_contact'] = array('windowsoutlook'); $config['activesync_multifolder_blacklist_note'] = null; $config['activesync_multifolder_blacklist_task'] = null;
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/ItemOperations.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/ItemOperations.php
Changed
@@ -59,9 +59,6 @@ $this->_emptyFolderContents[] = $this->_handleEmptyFolderContents($emptyFolderContents); } } - - if ($this->_logger instanceof Zend_Log) - $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " fetches: " . print_r($this->_fetches, true)); } /** @@ -118,7 +115,7 @@ $fileReference = $dataController->getFileReference($fetch['fileReference']); // unset data field and move content to stream - if ($this->_requestParameters['acceptMultipart'] == true) { + if (!empty($this->_requestParameters['acceptMultipart'])) { $this->_headers['Content-Type'] = 'application/vnd.ms-sync.multipart'; $partStream = fopen("php://temp", 'r+');
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/MeetingResponse.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/MeetingResponse.php
Changed
@@ -50,9 +50,6 @@ ); } } - - if ($this->_logger instanceof Zend_Log) - $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " results: " . print_r($this->_results, true)); } /**
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/MoveItems.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/MoveItems.php
Changed
@@ -46,9 +46,6 @@ 'dstFldId' => (string)$move->DstFldId ); } - - if ($this->_logger instanceof Zend_Log) - $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " moves: " . print_r($this->_moves, true)); } /**
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/Ping.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/Ping.php
Changed
@@ -141,6 +141,10 @@ // reconnect external connections, etc. call_user_func($wakeupCallback); + // Calculate secondsLeft before any loop break just to have a correct value + // for logging purposes in case we breaked from the loop early + $secondsLeft = $intervalEnd - time(); + try { $device = $this->_deviceBackend->get($this->_device->id); } catch (Syncroton_Exception_NotFound $e) { @@ -237,7 +241,8 @@ if ($status != self::STATUS_NO_CHANGES_FOUND) { break; } - + + // Update secondsLeft (again) $secondsLeft = $intervalEnd - time(); if ($this->_logger instanceof Zend_Log)
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/Search.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/Search.php
Changed
@@ -39,9 +39,6 @@ $xml = simplexml_import_dom($this->_requestBody); $this->_store = new Syncroton_Model_StoreRequest($xml->Store); - - if ($this->_logger instanceof Zend_Log) - $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " stores: " . print_r($this->_store, true)); } /**
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/SendMail.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/SendMail.php
Changed
@@ -32,7 +32,7 @@ */ public function handle() { - if ($this->_requestParameters['contentType'] == 'message/rfc822') { + if (!empty($this->_requestParameters['contentType']) && $this->_requestParameters['contentType'] == 'message/rfc822') { $this->_mime = $this->_requestBody; $this->_saveInSent = $this->_requestParameters['saveInSent']; $this->_replaceMime = false;
View file
kolab-syncroton-2.3.15.tar.gz/lib/ext/Syncroton/Command/Wbxml.php -> kolab-syncroton-2.3.16.tar.gz/lib/ext/Syncroton/Command/Wbxml.php
Changed
@@ -143,8 +143,8 @@ $this->_requestBody = $requestBody; $this->_device = $device; $this->_requestParameters = $requestParameters; - $this->_policyKey = $requestParameters['policyKey']; - + $this->_policyKey = isset($requestParameters['policyKey']) ? $requestParameters['policyKey'] : null; + $this->_deviceBackend = Syncroton_Registry::getDeviceBackend(); $this->_folderBackend = Syncroton_Registry::getFolderBackend(); $this->_syncStateBackend = Syncroton_Registry::getSyncStateBackend();
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync.php
Changed
@@ -46,7 +46,7 @@ public $password; const CHARSET = 'UTF-8'; - const VERSION = "2.3.15"; + const VERSION = "2.3.16"; /** @@ -232,7 +232,9 @@ $err_str = $this->get_storage()->get_error_str(); } - kolab_auth::log_login_error($auth['user'], $err_str ?: $err); + if (class_exists('kolab_auth', false)) { + kolab_auth::log_login_error($auth['user'], $err_str ?: $err); + } $this->plugins->exec_hook('login_failed', array( 'host' => $auth['host'],
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_data.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_data.php
Changed
@@ -428,6 +428,11 @@ // Remove subfolders if (!empty($options['deleteSubFolders'])) { $list = $this->listFolders($folderid); + + if (!is_array($list)) { + throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR); + } + foreach ($list as $folderid => $folder) { $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); $folder = $this->getFolderObject($foldername); @@ -508,7 +513,7 @@ $oldEntry = $this->getObject($folderId, $serverId); if (empty($oldEntry)) { - throw new Syncroton_Exception_NotFound('id not found'); + throw new Syncroton_Exception_NotFound('entry not found'); } $entry = $this->toKolab($entry, $folderId, $oldEntry); @@ -947,6 +952,10 @@ { $folders = $this->extractFolders($folderid); + if (empty($folders)) { + return null; + } + foreach ($folders as $folderid) { $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); $folder = $this->getFolderObject($foldername); @@ -1125,7 +1134,7 @@ $this->device->deviceid, $this->modelName, $this->isMultiFolder()); } - if ($parentid === null) { + if ($parentid === null || !is_array($this->imap_folders)) { return $this->imap_folders; } @@ -1675,8 +1684,14 @@ */ protected function recurrence_to_kolab($data, $folderid, $timezone = null) { - if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) || !isset($data->recurrence->type)) { - return null; + if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence) + && !($data->recurrence instanceof Syncroton_Model_TaskRecurrence) + ) { + return; + } + + if (!isset($data->recurrence->type)) { + return; } $recurrence = $data->recurrence;
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_data_calendar.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_data_calendar.php
Changed
@@ -592,11 +592,8 @@ ); if ($status = $status_map[$request->userResponse]) { - // extract event data from the invitation - $event = $this->get_event_from_invitation($request); - - // find the event in calendar - $existing = $this->find_event_by_uid($event['uid']); + // extract event from the invitation + list($event, $existing) = $this->get_event_from_invitation($request); /* switch ($status) { case 'ACCEPTED': $event['free_busy'] = 'busy'; break; @@ -669,19 +666,26 @@ } /** - * Get an event from the invitation email + * Get an event from the invitation email or calendar folder */ protected function get_event_from_invitation(Syncroton_Model_MeetingResponse $request) { - // Limitations: - // 1. The meeting request may be in an iTip or the calendar event - // For now we support iTips only here - // 2. LongId might be used instead of RequestId, this is not supported + // Limitation: LongId might be used instead of RequestId, this is not supported + if ($request->requestId) { $mail_class = new kolab_sync_data_email($this->device, $this->syncTimeStamp); + // Event from an invitation email if ($event = $mail_class->get_invitation_event($request->requestId)) { - return $event; + // find the event in calendar + $existing = $this->find_event_by_uid($event['uid']); + + return array($event, $existing); + } + + // Event from calendar folder + if ($event = $this->getObject($request->collectionId, $request->requestId, $folder)) { + return array($event, $event); } throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); @@ -721,8 +725,6 @@ throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST); } - $this->update_attendee_status($old, $status); - if ($event['free_busy']) { $old['free_busy'] = $event['free_busy']; } @@ -731,10 +733,8 @@ // to an iTip request with bumped SEQUENCE $old['sequence'] += 1; - // TODO: Free/busy trigger? - // Update the event - return $this->save_event($old); + return $this->save_event($old, $status); } /** @@ -744,7 +744,7 @@ protected function save_event(&$event, $status = null) { // Find default folder to which we'll save the event - if (empty($event['_mailbox'])) { + if (!isset($event['_mailbox'])) { $folders = $this->listFolders(); $storage = rcube::get_instance()->get_storage(); @@ -774,6 +774,8 @@ $this->update_attendee_status($event, $status); } + // TODO: Free/busy trigger? + if (isset($event['_mailbox'])) { $folder = $this->getFolderObject($event['_mailbox']);
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_data_contacts.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_data_contacts.php
Changed
@@ -168,7 +168,7 @@ if ($value) { // ActiveSync limits photo size to 48KB (of base64 encoded string) if (strlen($value) * 1.33 > 48 * 1024) { - continue; + continue 2; } } break;
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_data_email.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_data_email.php
Changed
@@ -131,6 +131,10 @@ $headers = $message->headers; // rcube_message_header + $this->storage->set_folder($message->folder); + + $this->logger->debug(sprintf("Processing message %s (size: %.2f MB)", $serverId, $headers->size / 1024 / 1024)); + // Calendar namespace fields foreach ($this->mapping as $key => $name) { $value = null; @@ -187,6 +191,8 @@ 'flagType' => 'FollowUp', 'status' => Syncroton_Model_EmailFlag::STATUS_ACTIVE, )); + } else { + $result['flag'] = new Syncroton_Model_EmailFlag(); } // Importance/Priority @@ -274,15 +280,22 @@ // only it's estimated size if (empty($prefs)) { $messageBody = ''; - $real_length = $message->size; + $real_length = $headers->size; $truncateAt = 0; $body_length = 0; $isTruncated = 1; } else if ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) { - $messageBody = $this->storage->get_raw_body($message->uid); + // Check if we have enough memory to handle the message + $messageBody = $this->message_mem_check($message, $headers->size); + + if (empty($messageBody)) { + $messageBody = $this->storage->get_raw_body($message->uid); + } + // make the source safe (Bug #2715, #2757) $messageBody = kolab_sync_message::recode_message($messageBody); + // strip out any non utf-8 characters $messageBody = rcube_charset::clean($messageBody); $real_length = $body_length = strlen($messageBody); @@ -1231,12 +1244,17 @@ $entryid = $entryid['itemId']; } + // Note: the id might be in a form of <folder>::<uid>[::<part_id>] list($folderid, $uid) = explode('::', $entryid); + + if (empty($uid)) { + return; + } + $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid); if ($foldername === null || $foldername === false) { - // @TODO exception? - return null; + return; } return array( @@ -1283,12 +1301,11 @@ protected function getMessagePartBody($message, $part, $html = false) { // Check if we have enough memory to handle the message in it - // @FIXME: we need up to 5x more memory than the body - if (!rcube_utils::mem_check($part->size * 5)) { - return ''; - } + $body = $this->message_mem_check($message, $part->size, false); - $body = $message->get_part_body($part->mime_id, true); + if ($body !== false) { + $body = $message->get_part_body($part->mime_id, true); + } // message is cached but not exists, or other error if ($body === false) { @@ -1643,4 +1660,64 @@ } } } + + /** + * Checks if the message can be processed, depending on its size and + * memory_limit, otherwise throws an exception or returns fake body. + */ + protected function message_mem_check($message, $size, $result = null) + { + static $memory_rised; + + // @FIXME: we need up to 5x more memory than the body + // Note: Biggest memory multiplication happens in recode_message() + // and the Syncroton engine (which also does not support passing bodies + // as streams). It also happens when parsing the plain/html text body + // in getMessagePartBody() though the footprint there is probably lower. + + if (!rcube_utils::mem_check($size * 5)) { + // If we already rised the memory we throw an exception, so the message + // will be synchronized in the next run (then we might have enough memory) + if ($memory_rised) { + throw new Syncroton_Exception_MemoryExhausted; + } + + $memory_rised = true; + $memory_max = 512; // maximum in MB + $memory_limit = round(parse_bytes(ini_get('memory_limit')) / 1024 / 1024); // current limit (in MB) + $memory_add = round($size * 5 / 1024 / 1024); // how much we need (in MB) + $memory_needed = min($memory_limit + $memory_add, $memory_max) . "M"; + + if ($memory_limit < $memory_max) { + $this->logger->debug("Setting memory_limit=$memory_needed"); + + if (ini_set('memory_limit', $memory_needed) !== false) { + // Memory has been rised, check again + if (rcube_utils::mem_check($size * 5)) { + return; + } + } + } + + $this->logger->warn("Not enough memory. Using fake email body."); + + if ($result !== null) { + return $result; + } + + // Let's return a fake message. If we return an empty body Outlook + // will not list the message at all. This way user can do something + // with the message (flag, delete, move) and see the reason why it's fake + // and importantly see its subject, sender, etc. + // TODO: Localization? + $msg = "This message is too large for ActiveSync."; + // $msg .= "See https://kb.kolabenterprise.com/documentation/some-place for more information."; + + // Get original message headers + $headers = $this->storage->get_raw_headers($message->uid); + + // Build a fake message with original headers, but changed body + return kolab_sync_message::fake_message($headers, $msg); + } + } }
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_data_gal.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_data_gal.php
Changed
@@ -153,7 +153,7 @@ } if (strlen($value) > $maxsize) { - continue; + continue 2; } $value = new Syncroton_Model_GALPicture(array(
View file
kolab-syncroton-2.3.15.tar.gz/lib/kolab_sync_message.php -> kolab-syncroton-2.3.16.tar.gz/lib/kolab_sync_message.php
Changed
@@ -347,6 +347,29 @@ } /** + * Creates a fake plain text message source with predefined headers and body + * + * @param string $headers Message headers + * @param string $body Plain text body + * + * @return string Message source + */ + public static function fake_message($headers, $body = '') + { + $hdrs = self::parse_headers($headers); + $result = ''; + + $hdrs['Content-Type'] = 'text/plain; charset=UTF-8'; + $hdrs['Content-Transfer-Encoding'] = 'quoted-printable'; + + foreach ($hdrs as $header => $header_value) { + $result .= $header . ': ' . $header_value . "\r\n"; + } + + return $result . "\r\n" . self::encode($body, 'quoted-printable'); + } + + /** * MIME message parser * * @param string|resource $message MIME message source
View file
kolab-syncroton.dsc
Changed
@@ -2,9 +2,9 @@ Source: kolab-syncroton Binary: kolab-syncroton Architecture: all -Version: 2.3.15-0~kolab6 -Maintainer: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> -Uploaders: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen@kolabsys.com> +Version: 2.3.16-0~kolab1 +Maintainer: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> +Uploaders: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> Homepage: http://www.kolab.org/ Standards-Version: 3.9.3 Vcs-Git: http://git.kolab.org/kolab-syncroton @@ -12,5 +12,5 @@ Package-List: kolab-syncroton deb utils extra Files: - 00000000000000000000000000000000 0 kolab-syncroton-2.3.15.tar.gz + 00000000000000000000000000000000 0 kolab-syncroton-2.3.16.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
.