composer.json config/config.ini.sample lib/Kolab public_html/index.php README.md

Thomas Brüderli bruederli at kolabsys.com
Tue May 6 18:00:00 CEST 2014


 README.md                                 |   22 ++-
 composer.json                             |    2 
 config/config.ini.sample                  |   27 +++
 lib/Kolab/FreeBusy/Directory.php          |   15 +-
 lib/Kolab/FreeBusy/FormatExchange2010.php |    2 
 lib/Kolab/FreeBusy/Source.php             |   36 ++++
 lib/Kolab/FreeBusy/SourceFile.php         |    5 
 lib/Kolab/FreeBusy/SourceIMAP.php         |  218 +++++++++++++++++++++++++++++-
 lib/Kolab/FreeBusy/Utils.php              |   69 +++++++++
 public_html/index.php                     |   27 ---
 10 files changed, 381 insertions(+), 42 deletions(-)

New commits:
commit 886215e40e6fd4e5c8994d069bc390840ab0e0bf
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Tue May 6 17:57:22 2014 +0200

    - Implement source type 'imap' that fetches free-busy data right from IMAP folders.
      This requires the Roundcube framework, config and plugins to be symlinked. Described in README
    - Add caching option for all sources + expire option for file source.

diff --git a/README.md b/README.md
index e395922..9db77b0 100644
--- a/README.md
+++ b/README.md
@@ -7,25 +7,37 @@ This package uses [Composer](http://getcomposer.org) to install and maintain req
 
 Execute this in the project root directory:
 
-$ curl -s http://getcomposer.org/installer | php
+`$ curl -s http://getcomposer.org/installer | php`
 
 This will create a file named composer.phar in the project directory.
 
 2. Install Dependencies
 
-$ php composer.phar install
+`$ php composer.phar install`
+
+2a. Link Roundcube framework and plugins
+
+If free-busy data is to be pulled from IMAP directly, the Roundcube framework, config
+and Kolab-specific plugins are required. Symlink them into the project directory:
+
+```
+$ ln -s /usr/share/roundcubemail/program/lib/Roundcube lib/Roundcube
+$ ln -s /usr/share/roundcubemail/plugins lib/plugins
+$ ln -s /etc/roundcubemail/defaults.inc.php config/defaults.inc.php
+$ ln -s /etc/roundcubemail/config.inc.php config/config.inc.php
+```
 
 3. Create local config
 
 Copy the config template file to config/config.ini:
 
-$ cp config/config.ini.sample config/config.ini
+`$ cp config/config.ini.sample config/config.ini`
 
 Edit the local config/config.ini file according to your setup and taste.
 
 4. Give write access for the webserver user to the 'log' folder:
 
-$ chown <www-user> log
+`$ chown <www-user> log`
 
-6. Configure your webserver to point to the 'web' directory of this package as document root.
+5. Configure your webserver to point to the 'web' directory of this package as document root.
 
diff --git a/composer.json b/composer.json
index 769b88d..91cad3b 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
 	"name": "kolab/free-busy",
 	"description": "Kolab Free/Busy Service",
 	"license": "AGPL-3.0",
-	"version": "0.1.3",
+	"version": "0.1.4",
 	"repositories": [
 		{
 			"type": "pear",
diff --git a/config/config.ini.sample b/config/config.ini.sample
index 778f15a..eeb7347 100644
--- a/config/config.ini.sample
+++ b/config/config.ini.sample
@@ -39,6 +39,12 @@ type = static
 filter = "@yourdomain"
 fbsource = file:/var/lib/kolab-freebusy/%s.ifb
 
+;; check if primary email address hits a cache file (saves LDAP lookups)
+[directory "local-cache"]
+type = static
+fbsource = file:/var/cache/kolab-freebusy/%s.ifb
+expires = 10m
+
 ;; local Kolab directory server
 [directory "kolab-ldap"]
 type = ldap
@@ -49,7 +55,21 @@ base_dn = "dc=yourdomain,dc=com"
 filter = "(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))"
 attributes = mail, sn
 lc_attributes = sn
-fbsource = file:/www/kolab-freebusy/data/%mail.ifb
+fbsource = file:/var/lib/kolab-freebusy/%mail.ifb
+loglevel = 200  ; Info
+
+;; resolve Kolab resources from LDAP and fetch calendar from IMAP
+[directory "kolab-resources"]
+type = ldap
+host = ldap://localhost:389
+bind_dn = "uid=kolab-service,ou=Special Users,dc=yourdomain,dc=com"
+bind_pw = "<service-bind-pw>"
+base_dn = "ou=Resources,dc=yourdomain,dc=com"
+filter = "(&(objectClass=kolabsharedfolder)(mail=%s))"
+attributes = mail, kolabtargetfolder
+fbsource = "imap://cyrus-admin:<admin-pass>@localhost/%kolabtargetfolder?acl=lrs"
+cacheto = /var/cache/kolab-freebusy/%mail.ifb
+expires = 10m
 loglevel = 100  ; Debug
 
 ;; external MS Exchange 2010 server
@@ -59,3 +79,8 @@ filter = "@microsoft.com$"
 fbsource = https://externalhost/free-busy/%s.ics
 format = Exchange2010
 
+;; further examples of fbsource URIs
+; - fetch data from another server by HTTP(s)
+; fbsource = "https://fb-service-user:imap-password@kolab-server/freebusy/%mail.ifb"
+; - read directoy from a users calendars (all) using IMAP proxy authentication
+; fbsource = "imap://%mail:<admin-pass>@localhost/?proxy_auth=cyrus-admin"
diff --git a/lib/Kolab/FreeBusy/Directory.php b/lib/Kolab/FreeBusy/Directory.php
index 8ac2c24..95ce87a 100644
--- a/lib/Kolab/FreeBusy/Directory.php
+++ b/lib/Kolab/FreeBusy/Directory.php
@@ -74,12 +74,25 @@ abstract class Directory
 		// resolve user record first
 		if ($user = $this->resolve($user)) {
 			$fbsource = $this->config['fbsource'];
-			if ($source = Source::Factory($fbsource)) {
+			if ($source = Source::Factory($fbsource, $this->config)) {
 				// forward request to Source instance
 				if ($data = $source->getFreeBusyData($this->postprocessAttrib($user), $extended)) {
 					// send data through the according format converter
 					$converter = Format::factory($this->config['format']);
 					$data = $converter->toVCalendar($data);
+
+					// cache the generated data
+					if ($data && $this->config['cacheto'] && !$source->isCached()) {
+						$path = preg_replace_callback(
+							'/%\{?([a-z0-9]+)\}?/',
+							function($m) use ($user) { return $user[$m[1]]; },
+							$this->config['cacheto']
+						);
+
+						if (!@file_put_contents($path, $data, LOCK_EX)) {
+							Logger::get('directory')->addError("Failed to write to cache file '" . $path . "'!");
+						}
+					}
 				}
 
 				return $data;
diff --git a/lib/Kolab/FreeBusy/FormatExchange2010.php b/lib/Kolab/FreeBusy/FormatExchange2010.php
index 55a04dd..f1a09fa 100644
--- a/lib/Kolab/FreeBusy/FormatExchange2010.php
+++ b/lib/Kolab/FreeBusy/FormatExchange2010.php
@@ -69,7 +69,7 @@ class FormatExchange2010 extends Format
 
 			// get the freebusy report
 			$freebusy = $fbgen->getResult();
-			$freebusy->PRODID = '-//kolab.org//NONSGML Kolab Server 3//EN';
+			$freebusy->PRODID = Utils::PRODID;
 			$freebusy->METHOD = 'PUBLISH';
 
 			// serialize to VCALENDAR format
diff --git a/lib/Kolab/FreeBusy/Source.php b/lib/Kolab/FreeBusy/Source.php
index 3a256a3..a0a915d 100644
--- a/lib/Kolab/FreeBusy/Source.php
+++ b/lib/Kolab/FreeBusy/Source.php
@@ -29,22 +29,24 @@ namespace Kolab\FreeBusy;
 abstract class Source
 {
 	protected $config = array();
+	protected $cached = false;
 
 	/**
 	 * Factory method creating an instace of Source according to config
 	 *
+	 * @param string Source URI
 	 * @param array Hash array with config
 	 */
-	public static function factory($url)
+	public static function factory($url, $conf)
 	{
 		$config = parse_url($url);
 		$config['url'] = $url;
 		switch ($config['scheme']) {
-			case 'file':	return new SourceFile($config);
+			case 'file':	return new SourceFile($config + $conf);
 			case 'imap':
-			case 'imaps':	return new SourceIMAP($config);
+			case 'imaps':	return new SourceIMAP($config + $conf);
 			case 'http':
-			case 'https':	return new SourceURL($config);
+			case 'https':	return new SourceURL($config + $conf);
 		}
 
 		Logger::get('source')->addError("Invalid source configuration: " . $url);
@@ -85,4 +87,30 @@ abstract class Source
 
 		return $config;
 	}
+
+	/**
+	 * Helper method to check if a cached file exists and is still valid
+	 *
+	 * @param array Hash array with (replaced) config properties
+	 * @return string Cached free-busy data or false if cache file doesn't exist or is expired
+	 */
+	protected function getCached($config)
+	{
+		if ($config['cacheto'] && file_exists($config['cacheto'])) {
+			if (empty($config['expires']) || filemtime($config['cacheto']) + Utils::getOffsetSec($config['expires']) >= time()) {
+				$this->cached = true;
+				return file_get_contents($config['cacheto']);
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * Return the value of the 'cached' flag
+	 */
+	public function isCached()
+	{
+		return $this->cached;
+	}
 }
\ No newline at end of file
diff --git a/lib/Kolab/FreeBusy/SourceFile.php b/lib/Kolab/FreeBusy/SourceFile.php
index af25b6e..5106322 100644
--- a/lib/Kolab/FreeBusy/SourceFile.php
+++ b/lib/Kolab/FreeBusy/SourceFile.php
@@ -38,7 +38,10 @@ class SourceFile extends Source
 
 		// deliver file contents if found
 		if (is_readable($config['path'])) {
-			return file_get_contents($config['path']);
+			// check expiration if configured
+			if (empty($this->config['expires']) || filemtime($config['path']) + Utils::getOffsetSec($this->config['expires']) > time()) {
+				return file_get_contents($config['path']);
+			}
 		}
 
 		// not found
diff --git a/lib/Kolab/FreeBusy/SourceIMAP.php b/lib/Kolab/FreeBusy/SourceIMAP.php
index c545423..494a4d5 100644
--- a/lib/Kolab/FreeBusy/SourceIMAP.php
+++ b/lib/Kolab/FreeBusy/SourceIMAP.php
@@ -23,19 +23,235 @@
 
 namespace Kolab\FreeBusy;
 
+use Sabre\VObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\FreeBusyGenerator;
+use Sabre\VObject\ParseException;
+
+// configure env for Roundcube framework
+define('RCUBE_INSTALL_PATH', KOLAB_FREEBUSY_ROOT . '/');
+define('RCUBE_CONFIG_DIR',   KOLAB_FREEBUSY_ROOT . '/config/');
+define('RCUBE_PLUGINS_DIR',  KOLAB_FREEBUSY_ROOT . '/lib/plugins/');
+
+
+
 /**
  * Implementation of a Free/Busy data source reading from IMAP
  * (not yet implemented!)
  */
 class SourceIMAP extends Source
 {
+	private $folders = array();
+
+	public function __construct($config)
+	{
+		parent::__construct($config);
+
+		// load the Roundcube framework with its autoloader
+		require_once KOLAB_FREEBUSY_ROOT . '/lib/Roundcube/bootstrap.php';
+
+		$rcube = \rcube::get_instance(\rcube::INIT_WITH_DB | \rcube::INIT_WITH_PLUGINS);
+
+		// Load plugins
+		$rcube->plugins->init($rcube);
+		$rcube->plugins->load_plugins(array(), array('libkolab','libcalendaring'));
+	}
+
 	/**
 	 * @see Source::getFreeBusyData()
 	 */
 	public function getFreeBusyData($user, $extended)
 	{
+		$log = Logger::get('imap', intval($this->config['loglevel']));
+
 		$config = $this->getUserConfig($user);
+		parse_str(strval($config['query']), $param);
+		$config += $param;
+
+		// log this...
+		$log->addInfo("Fetching data for ", $config);
+
+		// caching is enabled
+		if (!empty($config['cacheto'])) {
+			// check for cached data
+			if ($cached = $this->getCached($config)) {
+				$log->addInfo("Deliver cached data from " . $config['cacheto']);
+				return $cached;
+			}
+			// touch cache file to avoid multiple requests generating the same data
+			if (file_exists($config['cacheto'])) {
+				touch($config['cacheto']);
+			}
+			else {
+				file_put_contents($config['cacheto'], Utils::dummyVFreebusy($user['mail']));
+			}
+		}
+
+		// synchronize with IMAP and read Kolab event objects
+		if ($imap = $this->imap_login($config)) {
+			// target folder is specified in source URI
+			if ($config['path'] && $config['path'] != '/') {
+				$folders = array(\kolab_storage::get_folder(substr($config['path'], 1)));
+				$read_all = true;
+			}
+			else {  // list all folders of type 'event'
+				$folders = \kolab_storage::get_folders('event', false);
+				$read_all = false;
+			}
+
+			// make \libvcalendar class available
+			\libcalendaring::get_ical();
+
+			$utc      = new \DateTimezone('UTC');
+			$dtstart  = new \DateTime('now - 8 weeks 00:00:00', $utc);
+			$dtend    = new \DateTime('now + 16 weeks 00:00:00', $utc);
+			$calendar = VObject\Component::create('VCALENDAR');
+
+			$query = array(array('dtstart','>',$dtstart), array('dtend','<',$dtend));
+			foreach ($folders as $folder) {
+				$log->debug('Reading Kolab folder: ' . $folder->name, $folder->get_folder_info());
+
+				// skip other user's shared calendars
+				if (!$read_all && $folder->get_namespace() == 'other') {
+					continue;
+				}
+
+				// set ACL (temporarily)
+				if ($config['acl']) {
+					$folder->_old_acl = $folder->get_myrights();
+					$imap->set_acl($folder->name, $config['user'], $config['acl']);
+				}
+
+				foreach ($folder->select($query) as $event) {
+					$log->debug('Found event', $event);
+
+					if ($event['cancelled'])
+						continue;
+
+					// TODO: only consider shared namespace events if user is a confirmed participant
+					if (!$read_all && $folder->get_namespace() == 'shared') {
+						continue;  // skip all for now
+					}
+
+					// copied from libvcalendar::_to_ical()
+					$ve = VObject\Component::create('VEVENT');
+
+					// all-day events end the next day
+					if ($event['allday'] && !empty($event['end'])) {
+						$event['end'] = clone $event['end'];
+						$event['end']->add(new \DateInterval('P1D'));
+						$event['end']->_dateonly = true;
+					}
+					if (!empty($event['start']))
+						$ve->add(\libvcalendar::datetime_prop('DTSTART', $event['start'], false, (bool)$event['allday']));
+					if (!empty($event['end']))
+						$ve->add(\libvcalendar::datetime_prop('DTEND',   $event['end'], false, (bool)$event['allday']));
+
+					if (!empty($event['free_busy']))
+						$ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
+
+					if ($event['free_busy'] == 'tentative')
+						$ve->add('STATUS', 'TENTATIVE');
+					else if (!empty($event['status']))
+						$ve->add('STATUS', $event['status']);
+
+					if ($event['recurrence']) {
+						if ($exdates = $event['recurrence']['EXDATE'])
+							unset($event['recurrence']['EXDATE']);
+						if ($rdates = $event['recurrence']['RDATE'])
+							unset($event['recurrence']['RDATE']);
+
+						if ($event['recurrence']['FREQ'])
+							$ve->add('RRULE', \libcalendaring::to_rrule($event['recurrence']));
+
+						// add EXDATEs each one per line (for Thunderbird Lightning)
+						if ($exdates) {
+							foreach ($exdates as $ex) {
+								if ($ex instanceof \DateTime) {
+									$exd = clone $event['start'];
+									$exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
+									$exd->setTimeZone($utc);
+									$ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
+								}
+							}
+						}
+						// add RDATEs
+						if (!empty($rdates)) {
+							$sample = \libvcalendar::datetime_prop('RDATE', $rdates[0]);
+							$rdprop = new VObject\Property\MultiDateTime('RDATE', null);
+							$rdprop->setDateTimes($rdates, $sample->getDateType());
+							$ve->add($rdprop);
+						}
+					}
+
+					// append to vcalendar container
+					$calendar->add($ve);
+				}
+			}
+
+			$this->imap_disconnect($imap, $config, $folders);
+
+			// feed the calendar object into the free/busy generator
+			// we must specify a start and end date, because recurring events are expanded. nice!
+			$fbgen = new FreeBusyGenerator($dtstart, $dtend, $calendar);
+
+			// get the freebusy report
+			$freebusy = $fbgen->getResult();
+			$freebusy->PRODID = Utils::PRODID;
+			$freebusy->METHOD = 'PUBLISH';
+			$freebusy->VFREEBUSY->ORGANIZER = 'mailto:' . $user['mail'];
+
+			// serialize to VCALENDAR format
+			return $freebusy->serialize();
+		}
+		// remove (temporary) cache file again
+		else if (!empty($config['cacheto']) && file_exists($config['cacheto'])) {
+			unlink($config['cacheto']);
+		}
+
+		return false;
+	}
+
+	/**
+	 * Helper method to establish connection to the configured IMAP backend
+	 */
+	private function imap_login($config)
+	{
+		$rcube = \rcube::get_instance();
+		$imap  = $rcube->get_storage();
+		$host  = $config['host'];
+		$port  = $config['port'] ?: ($config['scheme'] == 'imaps' ? 993 : 143);
+		$ssl   = $config['scheme'] == 'imaps' || $port == 993;
+
+		// enable proxy authentication
+		if (!empty($config['proxy_auth'])) {
+			$imap->set_options(array('auth_cid' => $config['proxy_auth'], 'auth_pw' => $config['pass']));
+		}
+
+		// authenticate user in IMAP
+		if (!$imap->connect($host, $config['user'], $config['pass'], $port, $ssl)) {
+			Logger::get('imap')->addWarning("Failed to connect to IMAP server: " . $imap->get_error_code(), $config);
+			return false;
+		}
+
+		// fake user object to rcube framework
+		$rcube->set_user(new \rcube_user('0', array('username' => $config['user'])));
+
+		return $imap;
+	}
+
+	/**
+	 * Cleanup and close IMAP connection
+	 */
+	private function imap_disconnect($imap, $config, $folders)
+	{
+		// reset ACL
+		if ($config['acl'] && !empty($folders)) {
+			foreach ($folders as $folder) {
+				$imap->set_acl($folder->name, $config['user'], $folder->_old_acl);
+			}
+		}
 
-		// TODO: implement this
+		$imap->close();
 	}
 }
diff --git a/lib/Kolab/FreeBusy/Utils.php b/lib/Kolab/FreeBusy/Utils.php
index 3fea324..0d76ab6 100644
--- a/lib/Kolab/FreeBusy/Utils.php
+++ b/lib/Kolab/FreeBusy/Utils.php
@@ -5,7 +5,7 @@
  *
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2013-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -28,6 +28,8 @@ namespace Kolab\FreeBusy;
  */
 class Utils
 {
+	const PRODID = '-//kolab.org//NONSGML Kolab Free-Busy Service 3.2//EN';
+
 	/**
 	 * Resolve the given directory to a real path ending with $append
 	 *
@@ -69,7 +71,7 @@ class Utils
 	 * Checks if the given IP address is in one of the provided ranges
 	 *
 	 * @param string   IP address
-	 * @param array    List of IP ranges/subnets to check against
+	 * @param array	List of IP ranges/subnets to check against
 	 * @return boolean True if in range, False if not
 	 */
 	public static function checkIPRange($ip, $ranges)
@@ -136,4 +138,67 @@ class Utils
 		return $binaryip;
 	}
 
+	/**
+	 * Returns number of seconds for a specified offset string.
+	 *
+	 * @param string String representation of the offset (e.g. 20min, 5h, 2days, 1week)
+	 * @return int   Number of seconds
+	 */
+	public static function getOffsetSec($str)
+	{
+		if (preg_match('/^([0-9]+)\s*([smhdw])/i', $str, $regs)) {
+			$amount = (int) $regs[1];
+			$unit   = strtolower($regs[2]);
+		}
+		else {
+			$amount = (int) $str;
+			$unit   = 's';
+		}
+
+		switch ($unit) {
+		case 'w':
+			$amount *= 7;
+		case 'd':
+			$amount *= 24;
+		case 'h':
+			$amount *= 60;
+		case 'm':
+			$amount *= 60;
+		}
+
+		return $amount;
+	}
+
+	/**
+	 * Returns an apparent empty Free/Busy list for the given user
+	 */
+	public static function dummyVFreebusy($user)
+	{
+		$now = time();
+		$dtformat = 'Ymd\THis\Z';
+
+		// NOTE: The following settings should probably correspond with
+		// whatever period of time kolab-freebusyd thinks it should use.
+
+		// Should probably be a setting. For now, do 8 weeks in the past
+		$start = $now - (60 * 60 * 24 * 7 * 8);
+		// Should probably be a setting. For now, do 16 weeks into the future
+		$end = $now + (60 * 60 * 24 * 7 * 16);
+
+		$dummy  = "BEGIN:VCALENDAR\n";
+		$dummy .= "VERSION:2.0\n";
+		$dummy .= "PRODID:" . self::PRODID . "\n";
+		$dummy .= "METHOD:PUBLISH\n";
+		$dummy .= "BEGIN:VFREEBUSY\n";
+		$dummy .= "ORGANIZER:MAILTO:" . $user . "\n";
+		$dummy .= "DTSTAMP:" . gmdate($dtformat) . "\n";
+		$dummy .= "DTSTART:" . gmdate($dtformat, $start) . "\n";
+		$dummy .= "DTEND:"   . gmdate($dtformat, $end) . "\n";
+		$dummy .= "COMMENT:This is a dummy vfreebusy that indicates an empty calendar\n";
+		$dummy .= "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
+		$dummy .= "END:VFREEBUSY\n";
+		$dummy .= "END:VCALENDAR\n";
+
+		return $dummy;
+	}
 }
\ No newline at end of file
diff --git a/public_html/index.php b/public_html/index.php
index 14c549c..5f8058b 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -5,7 +5,7 @@
  *
  * This is the public API to provide Free/Busy information for Kolab users.
  *
- * @version 0.1.3
+ * @version 0.1.4
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
  * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
@@ -105,31 +105,8 @@ if ($config->valid()) {
 	else {
 		$log->addInfo("Returning empty Free/Busy list for user $user");
 
-		$now = time();
-		$dtformat = 'Ymd\THis\Z';
-
-		// NOTE: The following settings should probably correspond with
-		// whatever period of time kolab-freebusyd thinks it should use.
-
-		// Should probably be a setting. For now, do 8 weeks in the past
-		$start = $now - (60 * 60 * 24 * 7 * 8);
-		// Should probably be a setting. For now, do 16 weeks into the future
-		$end = $now + (60 * 60 * 24 * 7 * 16);
-
 		// Return an apparent empty Free/Busy list.
-		print "BEGIN:VCALENDAR\n";
-		print "VERSION:2.0\n";
-		print "PRODID:-//kolab.org//NONSGML Kolab Server 3//EN\n";
-		print "METHOD:PUBLISH\n";
-		print "BEGIN:VFREEBUSY\n";
-		print "ORGANIZER:MAILTO:" . $user . ".ifb\n";
-		print "DTSTAMP:" . gmdate($dtformat) . "\n";
-		print "DTSTART:" . gmdate($dtformat, $start) . "\n";
-		print "DTEND:" . gmdate($dtformat, $end) . "\n";
-		print "COMMENT:This is a dummy vfreebusy that indicates an empty calendar\n";
-		print "FREEBUSY:19700101T000000Z/19700101T000000Z\n";
-		print "END:VFREEBUSY\n";
-		print "END:VCALENDAR\n";
+		print Utils::dummyVFreebusy($user);
 	}
 }
 




More information about the commits mailing list