[CalendarServer-changes] [15404] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Dec 18 12:02:35 PST 2015


Revision: 15404
          http://trac.calendarserver.org//changeset/15404
Author:   cdaboo at apple.com
Date:     2015-12-18 12:02:34 -0800 (Fri, 18 Dec 2015)
Log Message:
-----------
New tool to dump the default config to a plist file, preserving key order and comments from the Python source.

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-stdconfig.plist
    CalendarServer/trunk/twistedcaldav/stdconfig.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/dumpconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_dumpconfig.py

Modified: CalendarServer/trunk/conf/caldavd-stdconfig.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-stdconfig.plist	2015-12-17 20:18:32 UTC (rev 15403)
+++ CalendarServer/trunk/conf/caldavd-stdconfig.plist	2015-12-18 20:02:34 UTC (rev 15404)
@@ -19,180 +19,337 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<!-- Public network address information -->
+	<!-- Public network address information
+
+	     This is the server's public network address, which is provided to clients
+	     in URLs and the like.  It may or may not be the network address that the
+	     server is listening to directly, though it is by default.  For example, it
+	     may be the address of a load balancer or proxy which forwards connections
+	     to the server. -->
+
+	<!-- Network host name. -->
 	<key>ServerHostName</key>
 	<string></string>
+
+	<!-- HTTP port (0 to disable HTTP) -->
 	<key>HTTPPort</key>
 	<integer>0</integer>
+
+	<!-- SSL port (0 to disable HTTPS) -->
 	<key>SSLPort</key>
 	<integer>0</integer>
+
+	<!-- Whether to listen on SSL port(s) -->
 	<key>EnableSSL</key>
 	<false/>
+
+	<!-- If True, all nonSSL requests redirected to an SSL Port -->
 	<key>RedirectHTTPToHTTPS</key>
 	<false/>
+
+	<!-- SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD -->
 	<key>SSLMethod</key>
 	<string>SSLv23_METHOD</string>
+
 	<key>SSLCiphers</key>
 	<string>RC4-SHA:HIGH:!ADH</string>
 
+	<!-- Max-age value for Strict-Transport-Security header; set to 0 to disable
+	     header. -->
 	<key>StrictTransportSecuritySeconds</key>
 	<integer>604800</integer>
 
-	<!-- Network address configuration information -->
+	<!-- Network address configuration information.
+
+	     This configures the actual network address that the server binds to. -->
+
 	<key>SocketFiles</key>
 	<dict>
 		<key>Enabled</key>
 		<false/>
+
+		<!-- Socket file to listen for secure requests on -->
+		<key>Secured</key>
+		<string>secured.sock</string>
+
+		<!-- Socket file to listen for insecure requests on -->
+		<key>Unsecured</key>
+		<string>unsecured.sock</string>
+
+		<key>Owner</key>
+		<string></string>
+
 		<key>Group</key>
 		<string></string>
-		<key>Owner</key>
-		<string></string>
+
 		<key>Permissions</key>
 		<integer>504</integer>
-		<key>Secured</key>
-		<string>secured.sock</string>
-		<key>Unsecured</key>
-		<string>unsecured.sock</string>
 	</dict>
+
 	<key>SocketRoot</key>
 	<string>/tmp/calendarserver</string>
 
+	<!-- List of IP addresses to bind to [empty = all] -->
 	<key>BindAddresses</key>
 	<array>
 	</array>
+
+	<!-- List of port numbers to bind to for HTTP [empty = same as "Port"] -->
 	<key>BindHTTPPorts</key>
 	<array>
 	</array>
+
+	<!-- List of port numbers to bind to for SSL [empty = same as "SSLPort"] -->
 	<key>BindSSLPorts</key>
 	<array>
 	</array>
+
+	<!-- File descriptors to inherit for HTTP requests [empty = don't inherit] -->
 	<key>InheritFDs</key>
 	<array>
 	</array>
+
+	<!-- File descriptors to inherit for HTTPS requests [empty = don't inherit] -->
 	<key>InheritSSLFDs</key>
 	<array>
 	</array>
+
+	<!-- Inherited file descriptor to call recvmsg() on to receive sockets (none =
+	     don't inherit) -->
 	<key>MetaFD</key>
 	<integer>0</integer>
+
+	<!-- Use a 'meta' FD, i.e. an FD to transmit other FDs to slave processes. -->
 	<key>UseMetaFD</key>
 	<true/>
 
-	<!-- Database configuration -->
+	<!-- True: database; False: files -->
 	<key>UseDatabase</key>
 	<true/>
 
+	<!-- Timeout transactions that take longer than the specified number of
+	     seconds. Zero means no timeouts. 5 minute default. -->
 	<key>TransactionTimeoutSeconds</key>
 	<integer>300</integer>
 
+	<!-- When a transactions times out tell HTTP clients clients to retry after
+	     this amount of time -->
 	<key>TransactionHTTPRetrySeconds</key>
 	<integer>300</integer>
 
+	<!-- 2 possible values: empty, meaning 'spawn postgres yourself', or
+	     'postgres', meaning 'connect to a postgres database as specified by the
+	     'DSN' configuration key.  Will support more values in the future. -->
 	<key>DBType</key>
 	<string></string>
 
+	<!-- The username to use when DBType is empty -->
 	<key>SpawnedDBUser</key>
 	<string>caldav</string>
 
+	<!-- Used to connect to an external database if DBType is non-empty -->
 	<key>DatabaseConnection</key>
 	<dict>
+		<!-- Database connection endpoint -->
+		<key>endpoint</key>
+		<string></string>
+
+		<!-- Name of database or Oracle SID -->
 		<key>database</key>
 		<string></string>
-		<key>endpoint</key>
+
+		<!-- User name to connect as -->
+		<key>user</key>
 		<string></string>
+
+		<!-- Password to use -->
 		<key>password</key>
 		<string></string>
-		<key>user</key>
-		<string></string>
 	</dict>
 
+	<!-- Internally used by database to tell slave processes to inherit a file
+	     descriptor and use it as an AMP connection over a UNIX socket; see
+	     twext.enterprise.adbapi2.ConnectionPoolConnection -->
 	<key>DBAMPFD</key>
 	<integer>0</integer>
 
+	<!-- Use a shared database connection pool in the master process, rather than
+	     having each client make its connections directly. -->
 	<key>SharedConnectionPool</key>
 	<false/>
 
+	<!-- Set to True to prevent the server or utility tools from running if the
+	     database needs a schema upgrade. -->
 	<key>FailIfUpgradeNeeded</key>
 	<true/>
 
+	<!-- Set to True to check the current database schema against the schema file
+	     matching the database schema version. -->
+	<key>CheckExistingSchema</key>
+	<false/>
+
+	<!-- When upgrading, only upgrade homes where the owner UID starts with the
+	     specified prefix. The upgrade will only be partial and only apply to
+	     upgrade pieces that affect entire homes. The upgrade will need to be run
+	     again without this prefix set to complete the overall upgrade. -->
 	<key>UpgradeHomePrefix</key>
 	<string></string>
 
 	<!-- Work queue configuration information -->
+
 	<key>WorkQueue</key>
 	<dict>
+		<!-- Interval in seconds for job queue polling -->
+		<key>queuePollInterval</key>
+		<real>0.1</real>
+
+		<!-- Number of seconds before an assigned job is considered overdue -->
+		<key>queueOverdueTimeout</key>
+		<integer>300</integer>
+
+		<!-- Array of array that describe the threshold and new polling interval for
+		     job queue polling back off -->
+		<key>queuePollingBackoff</key>
+		<array>
+			<array>
+				<integer>60</integer>
+				<integer>60</integer>
+			</array>
+			<array>
+				<integer>5</integer>
+				<integer>1</integer>
+			</array>
+		</array>
+
+		<!-- Queue capacity (percentage) which causes job processing to halt -->
+		<key>overloadLevel</key>
+		<integer>95</integer>
+
+		<!-- Queue capacity (percentage) at which only high priority items are run -->
+		<key>highPriorityLevel</key>
+		<integer>80</integer>
+
+		<!-- Queue capacity (percentage) at which only high/medium priority items are
+		     run -->
+		<key>mediumPriorityLevel</key>
+		<integer>50</integer>
+
+		<!-- When a job fails, reschedule it this number of seconds in the future -->
 		<key>failureRescheduleInterval</key>
 		<integer>60</integer>
-		<key>highPriorityLevel</key>
-		<integer>80</integer>
+
+		<!-- When a job can't run because of a lock, reschedule it this number of
+		     seconds in the future -->
 		<key>lockRescheduleInterval</key>
 		<integer>60</integer>
-		<key>mediumPriorityLevel</key>
-		<integer>50</integer>
-		<key>overloadLevel</key>
-		<integer>95</integer>
-		<key>queueOverdueTimeout</key>
-		<integer>300</integer>
-		<key>queuePollInterval</key>
-		<real>0.1</real>
+
+		<!-- dict of work table name's, whose values are dicts containing "priority"
+		     and "weight" items to use for newly created work. -->
+		<key>workParameters</key>
+		<dict>
+		</dict>
 	</dict>
 
 	<!-- Types of service provided -->
+
+	<!-- Enable CalDAV service -->
 	<key>EnableCalDAV</key>
 	<true/>
+
+	<!-- Enable CardDAV service -->
 	<key>EnableCardDAV</key>
 	<true/>
+
+	<!-- When True override all other services and set the server into podding-only
+	     mode -->
 	<key>MigrationOnly</key>
 	<false/>
 
 	<!-- Data store -->
+
 	<key>ServerRoot</key>
 	<string>/var/db/caldavd</string>
+
 	<key>DataRoot</key>
 	<string>Data</string>
+
 	<key>DatabaseRoot</key>
 	<string>Database</string>
+
 	<key>AttachmentsRoot</key>
 	<string>Attachments</string>
+
 	<key>DocumentRoot</key>
 	<string>Documents</string>
+
 	<key>ConfigRoot</key>
 	<string>Config</string>
+
 	<key>LogRoot</key>
 	<string>/var/log/caldavd</string>
+
 	<key>RunRoot</key>
 	<string>/var/run/caldavd</string>
+
 	<key>WebCalendarRoot</key>
 	<string>/Applications/Server.app/Contents/ServerRoot/usr/share/collabd/webcal/public</string>
 
-	<!--  Quotas -->
+	<!-- Quotas -->
+
+	<!-- Attachments -->
+	<!-- User attachment quota (in bytes - default 100MB) -->
 	<key>UserQuota</key>
 	<integer>104857600</integer>
+
+	<!-- Maximum size for a single attachment (in bytes - default 10MB) -->
 	<key>MaximumAttachmentSize</key>
 	<integer>10485760</integer>
 
+	<!-- Resource data -->
+	<!-- Maximum number of calendars/address books allowed in a home -->
 	<key>MaxCollectionsPerHome</key>
 	<integer>50</integer>
+
+	<!-- Maximum number of resources in a calendar/address book -->
 	<key>MaxResourcesPerCollection</key>
 	<integer>10000</integer>
+
+	<!-- Maximum resource size (in bytes) -->
 	<key>MaxResourceSize</key>
 	<integer>1048576</integer>
+
+	<!-- Maximum number of unique attendees -->
 	<key>MaxAttendeesPerInstance</key>
 	<integer>100</integer>
+
+	<!-- Maximum number of instances the server will index -->
 	<key>MaxAllowedInstances</key>
 	<integer>3000</integer>
 
+	<!-- Set to URL path of wiki authentication service, e.g. "/auth", in order to
+	     use javascript authentication dialog.  Empty string indicates standard
+	     browser authentication dialog should be used. -->
 	<key>WebCalendarAuthPath</key>
 	<string></string>
 
+	<!-- Define mappings of URLs to file system objects (directories or files) -->
 	<key>Aliases</key>
 	<array>
 	</array>
 
-	<!-- Directory service -->
+	<!-- Directory service
+
+	     A directory service provides information about principals (e.g. users,
+	     groups, locations and resources) to the server. -->
+
 	<key>DirectoryService</key>
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
+		<key>type</key>
+		<string>xml</string>
+
 		<key>params</key>
 		<dict>
 			<key>recordTypes</key>
@@ -200,24 +357,33 @@
 				<string>users</string>
 				<string>groups</string>
 			</array>
+
 			<key>xmlFile</key>
 			<string>accounts.xml</string>
 		</dict>
-		<key>type</key>
-		<string>xml</string>
 	</dict>
 
 	<key>DirectoryRealmName</key>
 	<string></string>
 
+	<!-- Apply an additional filter for attendee lookups where names must start
+	     with the search tokens rather than just contain them. -->
 	<key>DirectoryFilterStartsWith</key>
 	<false/>
 
-	<!-- Locations and Resources service -->
+	<!-- Locations and Resources service
+
+	     Supplements the directory service with information about locations and
+	     resources. -->
+
 	<key>ResourceService</key>
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
+		<key>type</key>
+		<string>xml</string>
+
 		<key>params</key>
 		<dict>
 			<key>recordTypes</key>
@@ -226,141 +392,217 @@
 				<string>resources</string>
 				<string>addresses</string>
 			</array>
+
 			<key>xmlFile</key>
 			<string>resources.xml</string>
 		</dict>
-		<key>type</key>
-		<string>xml</string>
 	</dict>
 
-	<!-- Augment service -->
+	<!-- Augment service
+
+	     Augments for the directory service records to add calendar specific
+	     attributes. -->
+
 	<key>AugmentService</key>
 	<dict>
+		<key>type</key>
+		<string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+
 		<key>params</key>
 		<dict>
-			<key>statSeconds</key>
-			<integer>15</integer>
 			<key>xmlFiles</key>
 			<array>
 				<string>augments.xml</string>
 			</array>
+
+			<key>statSeconds</key>
+			<integer>15</integer>
 		</dict>
-		<key>type</key>
-		<string>twistedcaldav.directory.augment.AugmentXMLDB</string>
 	</dict>
 
 	<!-- Proxies -->
+
+	<!-- Allows for initialization of the proxy database from an XML file -->
 	<key>ProxyLoadFromFile</key>
 	<string></string>
 
 	<!-- Special principals -->
+
+	<!-- Principals with "DAV:all" access (relative URLs) -->
 	<key>AdminPrincipals</key>
 	<array>
 	</array>
+
+	<!-- Principals with "DAV:read" access (relative URLs) -->
 	<key>ReadPrincipals</key>
 	<array>
 	</array>
+
+	<!-- Create "proxy access" principals -->
 	<key>EnableProxyPrincipals</key>
 	<true/>
 
 	<!-- Permissions -->
+
+	<!-- Allow unauthenticated read access to / -->
 	<key>EnableAnonymousReadRoot</key>
 	<true/>
+
+	<!-- Allow unauthenticated read access to hierarchy -->
 	<key>EnableAnonymousReadNav</key>
 	<false/>
+
+	<!-- Allow listing of principal collections -->
 	<key>EnablePrincipalListings</key>
 	<true/>
+
+	<!-- Render calendar collections as a monolithic iCalendar object -->
 	<key>EnableMonolithicCalendars</key>
 	<true/>
 
 	<!-- Client controls -->
+
+	<!-- List of regexes for clients to disallow -->
 	<key>RejectClients</key>
 	<array>
 	</array>
 
 	<!-- Authentication -->
+
 	<key>Authentication</key>
 	<dict>
+		<!-- Clear text; best avoided -->
 		<key>Basic</key>
 		<dict>
+			<key>Enabled</key>
+			<true/>
+
+			<!-- Advertised over non-SSL? -->
 			<key>AllowedOverWireUnencrypted</key>
 			<false/>
-			<key>Enabled</key>
-			<true/>
 		</dict>
+
+		<!-- Digest challenge/response -->
 		<key>Digest</key>
 		<dict>
+			<key>Enabled</key>
+			<true/>
+
 			<key>Algorithm</key>
 			<string>md5</string>
+
+			<key>Qop</key>
+			<string></string>
+
+			<!-- Advertised over non-SSL? -->
 			<key>AllowedOverWireUnencrypted</key>
 			<true/>
-			<key>Enabled</key>
-			<true/>
-			<key>Qop</key>
-			<string></string>
 		</dict>
+
+		<!-- Kerberos/SPNEGO -->
 		<key>Kerberos</key>
 		<dict>
-			<key>AllowedOverWireUnencrypted</key>
-			<true/>
 			<key>Enabled</key>
 			<false/>
+
 			<key>ServicePrincipal</key>
 			<string></string>
+
+			<!-- Advertised over non-SSL? -->
+			<key>AllowedOverWireUnencrypted</key>
+			<true/>
 		</dict>
+
+		<!-- TLS Client Certificate -->
 		<key>ClientCertificate</key>
 		<dict>
+			<key>Enabled</key>
+			<false/>
+
+			<!-- Advertised over non-SSL? -->
 			<key>AllowedOverWireUnencrypted</key>
 			<true/>
+
+			<!-- Always require a client cert -->
+			<key>Required</key>
+			<true/>
+
+			<!-- Array of acceptable client cert CA file names -->
 			<key>CAFiles</key>
 			<array>
 			</array>
-			<key>Enabled</key>
-			<false/>
-			<key>Required</key>
-			<true/>
+
+			<!-- Send the list of acceptable CAs to the client -->
 			<key>SendCAsToClient</key>
 			<true/>
 		</dict>
+
 		<key>Wiki</key>
 		<dict>
+			<key>Enabled</key>
+			<false/>
+
 			<key>Cookie</key>
 			<string>cc.collabd_session_guid</string>
-			<key>Enabled</key>
-			<false/>
+
 			<key>EndpointDescriptor</key>
 			<string>unix:path=/var/run/collabd</string>
 		</dict>
 	</dict>
 
 	<!-- Logging -->
+
+	<!-- Apache-style access log -->
 	<key>AccessLogFile</key>
 	<string>access.log</string>
+
+	<!-- Server activity log -->
 	<key>ErrorLogFile</key>
 	<string>error.log</string>
+
+	<!-- Agent activity log -->
 	<key>AgentLogFile</key>
 	<string>agent.log</string>
+
+	<!-- Utility log - name will be dynamically changed to executable name -->
 	<key>UtilityLogFile</key>
-	<string>.log</string>
+	<string>utility.log</string>
+
+	<!-- True = use log file, False = stdout -->
 	<key>ErrorLogEnabled</key>
 	<true/>
+
+	<!-- Rotate error log after so many megabytes -->
 	<key>ErrorLogRotateMB</key>
 	<integer>10</integer>
+
+	<!-- Retain this many error log files -->
 	<key>ErrorLogMaxRotatedFiles</key>
 	<integer>5</integer>
+
+	<!-- Rotate error log when service starts -->
+	<key>ErrorLogRotateOnStart</key>
+	<false/>
+
 	<key>PIDFile</key>
 	<string>caldavd.pid</string>
+
 	<key>RotateAccessLog</key>
 	<false/>
+
 	<key>EnableExtendedAccessLog</key>
 	<true/>
+
 	<key>EnableExtendedTimingAccessLog</key>
 	<false/>
+
 	<key>DefaultLogLevel</key>
 	<string></string>
+
 	<key>LogLevels</key>
 	<dict>
 	</dict>
+
 	<key>LogID</key>
 	<string></string>
 
@@ -368,194 +610,304 @@
 	<dict>
 		<key>HTTP</key>
 		<false/>
+
 		<key>iTIP</key>
 		<false/>
+
 		<key>iTIP-VFREEBUSY</key>
 		<false/>
+
 		<key>Implicit Errors</key>
 		<false/>
+
 		<key>AutoScheduling</key>
 		<false/>
+
 		<key>iSchedule</key>
 		<false/>
+
 		<key>migration</key>
 		<false/>
 	</dict>
+
 	<key>AccountingPrincipals</key>
 	<array>
 	</array>
+
 	<key>AccountingLogRoot</key>
 	<string>accounting</string>
 
 	<key>Stats</key>
 	<dict>
+		<key>EnableUnixStatsSocket</key>
+		<false/>
+
+		<key>UnixStatsSocket</key>
+		<string>caldavd-stats.sock</string>
+
 		<key>EnableTCPStatsSocket</key>
 		<false/>
-		<key>EnableUnixStatsSocket</key>
-		<false/>
+
 		<key>TCPStatsPort</key>
 		<integer>8100</integer>
-		<key>UnixStatsSocket</key>
-		<string>caldavd-stats.sock</string>
 	</dict>
 
 	<key>LogDatabase</key>
 	<dict>
 		<key>LabelsInSQL</key>
 		<false/>
-		<key>SQLStatements</key>
-		<false/>
+
 		<key>Statistics</key>
 		<false/>
+
 		<key>StatisticsLogFile</key>
 		<string>sqlstats.log</string>
+
+		<key>SQLStatements</key>
+		<false/>
+
 		<key>TransactionWaitSeconds</key>
 		<integer>0</integer>
 	</dict>
 
 	<!-- SSL/TLS -->
+
+	<!-- Public key -->
 	<key>SSLCertificate</key>
 	<string></string>
+
+	<!-- Private key -->
 	<key>SSLPrivateKey</key>
 	<string></string>
+
+	<!-- Certificate Authority Chain -->
 	<key>SSLAuthorityChain</key>
 	<string></string>
+
 	<key>SSLPassPhraseDialog</key>
 	<string>/etc/apache2/getsslpassphrase</string>
+
 	<key>SSLCertAdmin</key>
 	<string>/Applications/Server.app/Contents/ServerRoot/usr/sbin/certadmin</string>
 
+	<!-- Keychain identity to use in place of cert files -->
+	<key>SSLKeychainIdentity</key>
+	<string></string>
+
 	<!-- Process management -->
+
+	<!-- Username and Groupname to drop privileges to, if empty privileges will not
+	     be dropped. -->
 	<key>UserName</key>
 	<string></string>
+
 	<key>GroupName</key>
 	<string></string>
+
+	<!-- Multi-process -->
 	<key>ProcessType</key>
 	<string>Combined</string>
+
 	<key>MultiProcess</key>
 	<dict>
+		<key>ProcessCount</key>
+		<integer>0</integer>
+
 		<key>MinProcessCount</key>
 		<integer>2</integer>
+
 		<key>PerCPU</key>
 		<integer>1</integer>
+
 		<key>PerGB</key>
 		<integer>1</integer>
-		<key>ProcessCount</key>
-		<integer>0</integer>
+
 		<key>StaggeredStartup</key>
 		<dict>
 			<key>Enabled</key>
 			<false/>
+
 			<key>Interval</key>
 			<integer>15</integer>
 		</dict>
 	</dict>
 
+	<!-- How large a spawned process is allowed to get before it's stopped -->
 	<key>MemoryLimiter</key>
 	<dict>
+		<key>Enabled</key>
+		<true/>
+
+		<!-- How often to check memory sizes (in seconds) -->
+		<key>Seconds</key>
+		<integer>60</integer>
+
+		<!-- Memory limit (RSS in bytes) -->
 		<key>Bytes</key>
 		<integer>2147483648</integer>
-		<key>Enabled</key>
-		<true/>
+
+		<!-- True: only take into account resident memory; False: include virtual
+		     memory -->
 		<key>ResidentOnly</key>
 		<true/>
-		<key>Seconds</key>
-		<integer>60</integer>
 	</dict>
 
 	<!-- Service ACLs -->
 	<key>EnableSACLs</key>
 	<false/>
 
+	<!-- Make all data read-only -->
 	<key>EnableReadOnlyServer</key>
 	<false/>
 
 	<!-- Standard (or draft) WebDAV extensions -->
+
+	<!-- POST ;add-member extension -->
 	<key>EnableAddMember</key>
 	<true/>
+
+	<!-- REPORT collection-sync -->
 	<key>EnableSyncReport</key>
 	<true/>
+
+	<!-- REPORT collection-sync on home collections -->
 	<key>EnableSyncReportHome</key>
 	<true/>
+
+	<!-- Sync token includes config component -->
 	<key>EnableConfigSyncToken</key>
 	<true/>
+
+	<!-- /.well-known resource -->
 	<key>EnableWellKnown</key>
 	<true/>
+
+	<!-- Extended calendar-query REPORT -->
 	<key>EnableCalendarQueryExtended</key>
 	<true/>
 
+	<!-- Support Managed Attachments -->
 	<key>EnableManagedAttachments</key>
 	<false/>
 
+	<!-- server-info document -->
+	<key>EnableServerInfo</key>
+	<false/>
+
 	<!-- Generic CalDAV/CardDAV extensions -->
+
+	<!-- Allow clients to send/receive JSON jCal and jCard format data -->
 	<key>EnableJSONData</key>
 	<true/>
 
 	<!-- Non-standard CalDAV extensions -->
+
+	<!-- Calendar Drop Box -->
 	<key>EnableDropBox</key>
 	<false/>
+
+	<!-- Private Events -->
 	<key>EnablePrivateEvents</key>
 	<false/>
+
+	<!-- Old Timezone service -->
 	<key>EnableTimezoneService</key>
 	<false/>
 
+	<!-- New standard timezone service -->
 	<key>TimezoneService</key>
 	<dict>
-		<key>BasePath</key>
-		<string></string>
+		<!-- Overall on/off switch -->
 		<key>Enabled</key>
 		<true/>
+
+		<!-- URI where service is hosted -->
+		<key>URI</key>
+		<string>/stdtimezones</string>
+
+		<!-- Can be "primary" or "secondary" -->
 		<key>Mode</key>
 		<string>primary</string>
+
+		<!-- Path to directory containing a zoneinfo - if None use default package
+		     path secondary service MUST define its own writable path -->
+		<key>BasePath</key>
+		<string></string>
+
+		<!-- Path to db cache info - if None use default package path secondary
+		     service MUST define its own writable path if not None -->
+		<key>XMLInfoPath</key>
+		<string></string>
+
+		<!-- User friendly JSON output -->
 		<key>PrettyPrintJSON</key>
 		<true/>
+
 		<key>SecondaryService</key>
 		<dict>
+			<!-- Only one of these should be used when a secondary service is used -->
+			<!-- Domain/IP of secondary service to discover -->
 			<key>Host</key>
 			<string></string>
+
+			<!-- HTTP(s) URI to secondary service -->
 			<key>URI</key>
 			<string></string>
+
 			<key>UpdateIntervalMinutes</key>
 			<integer>1440</integer>
 		</dict>
-		<key>URI</key>
-		<string>/stdtimezones</string>
-		<key>XMLInfoPath</key>
-		<string></string>
 	</dict>
 
+	<!-- Strip out VTIMEZONES that are known -->
 	<key>EnableTimezonesByReference</key>
 	<true/>
+
+	<!-- Use timezone data from twistedcaldav.zoneinfo - don't copy to Data
+	     directory -->
 	<key>UsePackageTimezones</key>
 	<false/>
 
+	<!-- POST batch uploads -->
 	<key>EnableBatchUpload</key>
 	<true/>
+
+	<!-- Maximum number of resources in a batch POST -->
 	<key>MaxResourcesBatchUpload</key>
 	<integer>100</integer>
+
+	<!-- Maximum size of a batch POST (10 MB) -->
 	<key>MaxBytesBatchUpload</key>
 	<integer>10485760</integer>
 
 	<key>Sharing</key>
 	<dict>
+		<!-- Overall on/off switch -->
 		<key>Enabled</key>
 		<true/>
+
+		<!-- External (non-principal) sharees allowed -->
 		<key>AllowExternalUsers</key>
 		<false/>
 
 		<key>Calendars</key>
 		<dict>
+			<!-- Calendar on/off switch -->
 			<key>Enabled</key>
 			<true/>
+
 			<key>IgnorePerUserProperties</key>
 			<array>
 				<string>X-APPLE-STRUCTURED-LOCATION</string>
 			</array>
+
 			<key>CollectionProperties</key>
 			<dict>
-				<key>Global</key>
+				<key>Shadowable</key>
 				<array>
+					<string>{urn:ietf:params:xml:ns:caldav}calendar-description</string>
 				</array>
+
 				<key>ProxyOverride</key>
 				<array>
 					<string>{urn:ietf:params:xml:ns:caldav}calendar-description</string>
@@ -563,15 +915,18 @@
 					<string>{http://apple.com/ns/ical/}calendar-color</string>
 					<string>{http://apple.com/ns/ical/}calendar-order</string>
 				</array>
-				<key>Shadowable</key>
+
+				<key>Global</key>
 				<array>
-					<string>{urn:ietf:params:xml:ns:caldav}calendar-description</string>
 				</array>
 			</dict>
+
 			<key>Groups</key>
 			<dict>
+				<!-- Calendar sharing to groups on/off switch -->
 				<key>Enabled</key>
 				<true/>
+
 				<key>ReconciliationDelaySeconds</key>
 				<integer>5</integer>
 			</dict>
@@ -579,92 +934,135 @@
 
 		<key>AddressBooks</key>
 		<dict>
+			<!-- Address Book sharing on/off switch -->
 			<key>Enabled</key>
 			<false/>
+
 			<key>CollectionProperties</key>
 			<dict>
-				<key>Global</key>
+				<key>Shadowable</key>
 				<array>
+					<string>{urn:ietf:params:xml:ns:carddav}addressbook-description</string>
 				</array>
+
 				<key>ProxyOverride</key>
 				<array>
 				</array>
-				<key>Shadowable</key>
+
+				<key>Global</key>
 				<array>
-					<string>{urn:ietf:params:xml:ns:carddav}addressbook-description</string>
 				</array>
 			</dict>
+
 			<key>Groups</key>
 			<dict>
+				<!-- Address Book Group sharing on/off switch -->
 				<key>Enabled</key>
 				<false/>
 			</dict>
 		</dict>
 	</dict>
 
+	<!-- Only allow calendars to be created with a single component type If this is
+	     on, it will also trigger an upgrade behavior that will split existing
+	     calendars into multiples based on component type. If on, it will also
+	     cause new accounts to provision with separate calendars for events and
+	     tasks. -->
 	<key>RestrictCalendarsToOneComponentType</key>
 	<true/>
 
+	<!-- Set of supported iCalendar components -->
 	<key>SupportedComponents</key>
 	<array>
 		<string>VEVENT</string>
 		<string>VTODO</string>
 	</array>
 
+	<!-- Enable Trash Collection -->
 	<key>EnableTrashCollection</key>
 	<false/>
+
+	<!-- Expose Trash Collection as a resource -->
 	<key>ExposeTrashCollection</key>
 	<false/>
 
+	<!-- Perform upgrades - currently only the database to filesystem migration -
+	     but in the future, hopefully all relevant upgrades - in parallel in
+	     subprocesses. -->
 	<key>ParallelUpgrades</key>
 	<false/>
 
+	<!-- During the upgrade phase of startup, rather than skipping homes found both
+	     on the filesystem and in the database, merge the data from the filesystem
+	     into the database homes. -->
 	<key>MergeUpgrades</key>
 	<false/>
 
+	<!-- Support for default alarms generated by the server -->
 	<key>EnableDefaultAlarms</key>
 	<true/>
+
+	<!-- Remove duplicate alarms on PUT -->
 	<key>RemoveDuplicateAlarms</key>
 	<true/>
 
+	<!-- Remove duplicate private comments on PUT -->
 	<key>RemoveDuplicatePrivateComments</key>
 	<false/>
 
 	<key>HostedStatus</key>
 	<dict>
+		<!-- Decorate ATTENDEEs with the following parameter to indicate where the
+		     ATTENDEE is hosted, locally or externally.  It's configurable and
+		     extensible in case we want to add more values.  A value of empty string
+		     means no decoration. -->
 		<key>Enabled</key>
 		<false/>
+
 		<key>Parameter</key>
 		<string>X-APPLE-HOSTED-STATUS</string>
+
 		<key>Values</key>
 		<dict>
+			<key>local</key>
+			<string></string>
+
 			<key>external</key>
 			<string>EXTERNAL</string>
-			<key>local</key>
-			<string></string>
 		</dict>
 	</dict>
 
 	<key>RevisionCleanup</key>
 	<dict>
-		<key>CleanupPeriodDays</key>
-		<real>2.0</real>
 		<key>Enabled</key>
 		<true/>
+
+		<!-- Number of days that a client sync report token is valid -->
 		<key>SyncTokenLifetimeDays</key>
 		<real>14.0</real>
+
+		<!-- Number of days between revision cleanups -->
+		<key>CleanupPeriodDays</key>
+		<real>2.0</real>
 	</dict>
 
 	<key>InboxCleanup</key>
 	<dict>
-		<key>CleanupPeriodDays</key>
-		<real>2.0</real>
 		<key>Enabled</key>
 		<true/>
+
+		<!-- Number of days before deleting a new inbox item -->
+		<key>ItemLifetimeDays</key>
+		<real>14.0</real>
+
+		<!-- Number of days to keep an inbox item past the time when its referenced
+		     event ends -->
 		<key>ItemLifeBeyondEventEndDays</key>
 		<real>14.0</real>
-		<key>ItemLifetimeDays</key>
-		<real>14.0</real>
+
+		<!-- Number of days between inbox cleanups -->
+		<key>CleanupPeriodDays</key>
+		<real>2.0</real>
 	</dict>
 
 	<!-- CardDAV Features -->
@@ -672,57 +1070,83 @@
 	<dict>
 		<key>Enabled</key>
 		<true/>
-		<key>MaxQueryResults</key>
-		<integer>1000</integer>
-		<key>name</key>
-		<string>directory</string>
+
+		<key>type</key>
+		<string>twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService</string>
+
 		<key>params</key>
 		<dict>
-			<key>addDSAttrXProperties</key>
-			<false/>
-			<key>additionalAttributes</key>
-			<array>
-			</array>
-			<key>allowedAttributes</key>
-			<array>
-			</array>
-			<key>appleInternalServer</key>
-			<false/>
-			<key>cacheQuery</key>
-			<false/>
-			<key>cacheTimeout</key>
-			<integer>30</integer>
-			<key>dsLocalCacheTimeout</key>
-			<integer>30</integer>
-			<key>fakeETag</key>
+			<key>queryPeopleRecords</key>
 			<true/>
-			<key>ignoreSystemRecords</key>
+
+			<key>peopleNode</key>
+			<string>/Search/Contacts</string>
+
+			<key>queryUserRecords</key>
 			<true/>
-			<key>liveQuery</key>
-			<true/>
+
+			<key>userNode</key>
+			<string>/Search/Contacts</string>
+
 			<key>maxDSQueryRecords</key>
 			<integer>0</integer>
-			<key>peopleNode</key>
-			<string>/Search/Contacts</string>
+
 			<key>queryDSLocal</key>
 			<false/>
-			<key>queryPeopleRecords</key>
+
+			<key>ignoreSystemRecords</key>
 			<true/>
-			<key>queryUserRecords</key>
+
+			<key>dsLocalCacheTimeout</key>
+			<integer>30</integer>
+
+			<key>liveQuery</key>
 			<true/>
+
+			<key>fakeETag</key>
+			<true/>
+
+			<key>cacheQuery</key>
+			<false/>
+
+			<key>cacheTimeout</key>
+			<integer>30</integer>
+
 			<key>standardizeSyntheticUIDs</key>
 			<false/>
-			<key>userNode</key>
-			<string>/Search/Contacts</string>
+
+			<key>addDSAttrXProperties</key>
+			<false/>
+
+			<key>appleInternalServer</key>
+			<false/>
+
+			<key>additionalAttributes</key>
+			<array>
+			</array>
+
+			<key>allowedAttributes</key>
+			<array>
+			</array>
 		</dict>
-		<key>type</key>
-		<string>twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService</string>
+
+		<key>name</key>
+		<string>directory</string>
+
+		<key>MaxQueryResults</key>
+		<integer>1000</integer>
 	</dict>
+
+	<!-- /directory resource exists -->
 	<key>EnableSearchAddressBook</key>
 	<false/>
+
+	<!-- Anonymous users may access directory address book -->
 	<key>AnonymousDirectoryAddressBookAccess</key>
 	<false/>
 
+	<!-- /XXX CardDAV -->
+
 	<!-- Web-based administration -->
 	<key>EnableWebAdmin</key>
 	<true/>
@@ -732,23 +1156,38 @@
 	<false/>
 
 	<!-- Scheduling related options -->
+
 	<key>Scheduling</key>
 	<dict>
 		<key>CalDAV</key>
 		<dict>
+			<!-- Domain for mailto calendar user addresses on this server -->
 			<key>EmailDomain</key>
 			<string></string>
+
+			<!-- Domain for http calendar user addresses on this server -->
 			<key>HTTPDomain</key>
 			<string></string>
+
+			<!-- Regex patterns to match local calendar user addresses -->
 			<key>AddressPatterns</key>
 			<array>
 			</array>
+
+			<!-- Whether to maintain compatibility with non-implicit mode -->
 			<key>OldDraftCompatibility</key>
 			<true/>
+
+			<!-- Whether to support older clients that do not use Schedule-Tag feature -->
 			<key>ScheduleTagCompatibility</key>
 			<true/>
+
+			<!-- Private comments from attendees to organizer -->
 			<key>EnablePrivateComments</key>
 			<true/>
+
+			<!-- Names of iCalendar properties that are preserved when an Attendee does
+			     an invite PUT -->
 			<key>PerAttendeeProperties</key>
 			<array>
 				<string>X-APPLE-NEEDS-REPLY</string>
@@ -757,177 +1196,346 @@
 				<string>X-APPLE-TRAVEL-RETURN-DURATION</string>
 				<string>X-APPLE-TRAVEL-RETURN</string>
 			</array>
+
+			<!-- Names of X- iCalendar properties that are sent from ORGANIZER to
+			     ATTENDEE -->
 			<key>OrganizerPublicProperties</key>
 			<array>
 				<string>X-APPLE-DROPBOX</string>
 				<string>X-APPLE-STRUCTURED-LOCATION</string>
 			</array>
+
+			<!-- Names of X- iCalendar parameters that are sent from ORGANIZER to
+			     ATTENDEE -->
+			<key>OrganizerPublicParameters</key>
+			<array>
+			</array>
+
+			<!-- Names of X- iCalendar properties that are sent from ATTENDEE to
+			     ORGANIZER These are also implicitly added to OrganizerPublicProperties -->
+			<key>AttendeePublicProperties</key>
+			<array>
+			</array>
+
+			<!-- Names of X- iCalendar parameters that are sent from ATTENDEE to
+			     ORGANIZER These are also implicitly added to OrganizerPublicParameters -->
+			<key>AttendeePublicParameters</key>
+			<array>
+			</array>
 		</dict>
+
 		<key>iSchedule</key>
 		<dict>
+			<!-- iSchedule protocol -->
 			<key>Enabled</key>
 			<false/>
+
+			<!-- Reg-ex patterns to match iSchedule-able calendar user addresses -->
 			<key>AddressPatterns</key>
 			<array>
 			</array>
+
+			<!-- iSchedule server configurations -->
 			<key>RemoteServers</key>
 			<string>remoteservers.xml</string>
+
+			<!-- Capabilities serial number -->
 			<key>SerialNumber</key>
 			<integer>1</integer>
+
+			<!-- File where a fake Bind zone exists for creating fake DNS results -->
 			<key>DNSDebug</key>
 			<string></string>
+
+			<!-- DKIM options -->
 			<key>DKIM</key>
 			<dict>
+				<!-- DKIM signing/verification enabled -->
 				<key>Enabled</key>
 				<true/>
+
+				<!-- Domain for DKIM (defaults to ServerHostName) -->
 				<key>Domain</key>
 				<string></string>
+
+				<!-- Selector for public key -->
 				<key>KeySelector</key>
 				<string>ischedule</string>
+
+				<!-- Signature algorithm (one of rsa-sha1 or rsa-sha256) -->
 				<key>SignatureAlgorithm</key>
 				<string>rsa-sha256</string>
+
+				<!-- This server's public key stored in DNS -->
 				<key>UseDNSKey</key>
 				<true/>
+
+				<!-- This server's public key stored in HTTP /.well-known -->
 				<key>UseHTTPKey</key>
 				<true/>
+
+				<!-- This server's public key manually exchanged with others -->
 				<key>UsePrivateExchangeKey</key>
 				<true/>
+
+				<!-- Expiration time for signature verification -->
 				<key>ExpireSeconds</key>
 				<integer>3600</integer>
+
+				<!-- File where private key is stored -->
 				<key>PrivateKeyFile</key>
 				<string></string>
+
+				<!-- File where public key is stored -->
 				<key>PublicKeyFile</key>
 				<string></string>
+
+				<!-- Directory where private exchange public keys are stored -->
 				<key>PrivateExchanges</key>
 				<string></string>
+
+				<!-- Turn on protocol level debugging to return detailed information to the
+				     requestor -->
 				<key>ProtocolDebug</key>
 				<false/>
 			</dict>
 		</dict>
+
 		<key>iMIP</key>
 		<dict>
+			<!-- Server-to-iMIP protocol -->
 			<key>Enabled</key>
 			<false/>
+
 			<key>Sending</key>
 			<dict>
-				<key>Address</key>
+				<!-- SMTP server to relay messages through -->
+				<key>Server</key>
 				<string></string>
-				<key>Password</key>
-				<string></string>
+
+				<!-- SMTP server port to relay messages through -->
 				<key>Port</key>
 				<integer>587</integer>
-				<key>Server</key>
+
+				<!-- 'From' address for server -->
+				<key>Address</key>
 				<string></string>
-				<key>SuppressionDays</key>
-				<integer>7</integer>
+
 				<key>UseSSL</key>
 				<true/>
+
+				<!-- For account sending mail -->
 				<key>Username</key>
 				<string></string>
+
+				<!-- For account sending mail -->
+				<key>Password</key>
+				<string></string>
+
+				<!-- Messages for events older than this may days are not sent -->
+				<key>SuppressionDays</key>
+				<integer>7</integer>
 			</dict>
+
 			<key>Receiving</key>
 			<dict>
-				<key>Password</key>
+				<!-- Server to retrieve email messages from -->
+				<key>Server</key>
 				<string></string>
-				<key>PollingSeconds</key>
-				<integer>30</integer>
+
+				<!-- Server port to retrieve email messages from -->
 				<key>Port</key>
 				<integer>0</integer>
-				<key>Server</key>
-				<string></string>
+
+				<key>UseSSL</key>
+				<true/>
+
+				<!-- Type of message access server: 'pop' or 'imap' -->
 				<key>Type</key>
 				<string></string>
-				<key>UseSSL</key>
-				<true/>
+
+				<!-- How often to fetch mail -->
+				<key>PollingSeconds</key>
+				<integer>30</integer>
+
+				<!-- For account receiving mail -->
 				<key>Username</key>
 				<string></string>
+
+				<!-- For account receiving mail -->
+				<key>Password</key>
+				<string></string>
 			</dict>
+
+			<!-- Regex patterns to match iMIP-able calendar user addresses -->
 			<key>AddressPatterns</key>
 			<array>
 			</array>
+
+			<!-- Directory containing HTML templates for email invitations (invite.html,
+			     cancel.html) -->
 			<key>MailTemplatesDirectory</key>
 			<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/email_templates</string>
+
+			<!-- Directory containing language-specific subdirectories containing date-
+			     specific icons for email invitations -->
 			<key>MailIconsDirectory</key>
 			<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/date_icons</string>
+
+			<!-- How many days invitations are valid -->
 			<key>InvitationDaysToLive</key>
 			<integer>90</integer>
 		</dict>
+
 		<key>Options</key>
 		<dict>
+			<!-- Allow groups to be Organizers -->
 			<key>AllowGroupAsOrganizer</key>
 			<false/>
+
+			<!-- Allow locations to be Organizers -->
 			<key>AllowLocationAsOrganizer</key>
 			<false/>
+
+			<!-- Allow resources to be Organizers -->
+			<key>AllowResourceAsOrganizer</key>
+			<false/>
+
+			<!-- Allow locations to have events without an Organizer -->
 			<key>AllowLocationWithoutOrganizer</key>
 			<true/>
-			<key>AllowResourceAsOrganizer</key>
-			<false/>
+
+			<!-- Allow resources to have events without an Organizer -->
 			<key>AllowResourceWithoutOrganizer</key>
 			<true/>
+
+			<!-- Track who the last modifier of an unscheduled location event is -->
+			<key>TrackUnscheduledLocationData</key>
+			<true/>
+
+			<!-- Track who the last modifier of an unscheduled resource event is -->
+			<key>TrackUnscheduledResourceData</key>
+			<true/>
+
+			<!-- Maximum number of attendees to request freebusy for -->
+			<key>LimitFreeBusyAttendees</key>
+			<integer>30</integer>
+
+			<!-- Number of attendees to do batched refreshes: 0 - no batching -->
 			<key>AttendeeRefreshBatch</key>
 			<integer>5</integer>
+
+			<!-- Number of attendees above which attendee refreshes are suppressed: 0 -
+			     no limit -->
 			<key>AttendeeRefreshCountLimit</key>
 			<integer>50</integer>
+
+			<!-- Time for implicit UID lock timeout -->
+			<key>UIDLockTimeoutSeconds</key>
+			<integer>60</integer>
+
+			<!-- Expiration time for UID lock, -->
+			<key>UIDLockExpirySeconds</key>
+			<integer>300</integer>
+
+			<!-- Host names matched in http(s) CUAs -->
+			<key>PrincipalHostAliases</key>
+			<array>
+			</array>
+
+			<!-- Add a time stamp when an Attendee changes their PARTSTAT -->
+			<key>TimestampAttendeePartStatChanges</key>
+			<true/>
+
+			<!-- Delegates can get extra info in a freebusy request -->
+			<key>DelegeteRichFreeBusy</key>
+			<true/>
+
+			<!-- Any user can get extra info for rooms/resources in a freebusy request -->
+			<key>RoomResourceRichFreeBusy</key>
+			<true/>
+
 			<key>AutoSchedule</key>
 			<dict>
+				<!-- Auto-scheduling will never occur if set to False -->
 				<key>Enabled</key>
 				<true/>
+
+				<!-- Override augments setting and always auto-schedule -->
+				<key>Always</key>
+				<false/>
+
+				<!-- Allow auto-schedule for users -->
 				<key>AllowUsers</key>
 				<false/>
-				<key>Always</key>
-				<false/>
+
+				<!-- Default mode for auto-schedule processing, one of: "none"            -
+				     no auto-scheduling "accept-always"   - always accept, ignore busy time
+				     "decline-always"  - always decline, ignore free time "accept-if-free"
+				     - accept if free, do nothing if busy "decline-if-busy" - decline if
+				     busy, do nothing if free "automatic"       - accept if free, decline if
+				     busy -->
 				<key>DefaultMode</key>
 				<string>automatic</string>
+
+				<!-- How far into the future to check for booking conflicts -->
 				<key>FutureFreeBusyDays</key>
 				<integer>1095</integer>
 			</dict>
-			<key>DelegeteRichFreeBusy</key>
-			<true/>
-			<key>LimitFreeBusyAttendees</key>
-			<integer>30</integer>
-			<key>PrincipalHostAliases</key>
-			<array>
-			</array>
-			<key>RoomResourceRichFreeBusy</key>
-			<true/>
-			<key>Splitting</key>
-			<dict>
-				<key>Enabled</key>
-				<false/>
-				<key>Delay</key>
-				<integer>60</integer>
-				<key>PastDays</key>
-				<integer>14</integer>
-				<key>Size</key>
-				<integer>102400</integer>
-			</dict>
-			<key>TimestampAttendeePartStatChanges</key>
-			<true/>
-			<key>TrackUnscheduledLocationData</key>
-			<true/>
-			<key>TrackUnscheduledResourceData</key>
-			<true/>
-			<key>UIDLockExpirySeconds</key>
-			<integer>300</integer>
-			<key>UIDLockTimeoutSeconds</key>
-			<integer>60</integer>
+
 			<key>WorkQueues</key>
 			<dict>
+				<!-- Work queues for scheduling enabled -->
 				<key>Enabled</key>
 				<true/>
+
+				<!-- Number of seconds delay for a queued scheduling request/cancel -->
+				<key>RequestDelaySeconds</key>
+				<integer>5</integer>
+
+				<!-- Number of seconds delay for a queued scheduling reply -->
+				<key>ReplyDelaySeconds</key>
+				<integer>1</integer>
+
+				<!-- Time delay for sending an auto reply iTIP message -->
+				<key>AutoReplyDelaySeconds</key>
+				<integer>5</integer>
+
+				<!-- Time after an iTIP REPLY for first batched attendee refresh -->
 				<key>AttendeeRefreshBatchDelaySeconds</key>
 				<integer>5</integer>
+
+				<!-- Time between attendee batch refreshes -->
 				<key>AttendeeRefreshBatchIntervalSeconds</key>
 				<integer>5</integer>
-				<key>AutoReplyDelaySeconds</key>
-				<integer>5</integer>
+
+				<!-- Delay in seconds before a work item is executed again after a temp
+				     failure -->
+				<key>TemporaryFailureDelay</key>
+				<integer>60</integer>
+
+				<!-- Max number of temp failure retries before treating as a permanent
+				     failure -->
 				<key>MaxTemporaryFailures</key>
 				<integer>10</integer>
-				<key>ReplyDelaySeconds</key>
-				<integer>1</integer>
-				<key>RequestDelaySeconds</key>
-				<integer>5</integer>
-				<key>TemporaryFailureDelay</key>
+			</dict>
+
+			<key>Splitting</key>
+			<dict>
+				<!-- False for now whilst we experiment with this -->
+				<key>Enabled</key>
+				<false/>
+
+				<!-- Consider splitting when greater than 100KB -->
+				<key>Size</key>
+				<integer>102400</integer>
+
+				<!-- Number of days in the past where the split will occur -->
+				<key>PastDays</key>
+				<integer>14</integer>
+
+				<!-- How many seconds to delay the split work item -->
+				<key>Delay</key>
 				<integer>60</integer>
 			</dict>
 		</dict>
@@ -935,86 +1543,137 @@
 
 	<key>FreeBusyURL</key>
 	<dict>
-		<key>AnonymousAccess</key>
-		<false/>
+		<!-- Per-user free-busy-url protocol -->
 		<key>Enabled</key>
 		<false/>
+
+		<!-- Number of days into the future to generate f-b data if no explicit time-
+		     range is specified -->
 		<key>TimePeriod</key>
 		<integer>14</integer>
+
+		<!-- Allow anonymous read access to free-busy URL -->
+		<key>AnonymousAccess</key>
+		<false/>
 	</dict>
 
 	<!-- Notifications -->
+
 	<key>Notifications</key>
 	<dict>
+		<key>Enabled</key>
+		<false/>
+
 		<key>CoalesceSeconds</key>
 		<integer>3</integer>
-		<key>Enabled</key>
-		<false/>
+
 		<key>Services</key>
 		<dict>
 			<key>APNS</key>
 			<dict>
 				<key>Enabled</key>
 				<false/>
+
 				<key>SubscriptionURL</key>
 				<string>apns</string>
+
+				<!-- How often the client should re-register (2 days) -->
 				<key>SubscriptionRefreshIntervalSeconds</key>
 				<integer>172800</integer>
+
+				<!-- How often a purge is done (12 hours) -->
 				<key>SubscriptionPurgeIntervalSeconds</key>
 				<integer>43200</integer>
+
+				<!-- How old a subscription must be before it's purged (14 days) -->
 				<key>SubscriptionPurgeSeconds</key>
 				<integer>1209600</integer>
+
 				<key>ProviderHost</key>
 				<string>gateway.push.apple.com</string>
+
 				<key>ProviderPort</key>
 				<integer>2195</integer>
+
 				<key>FeedbackHost</key>
 				<string>feedback.push.apple.com</string>
+
 				<key>FeedbackPort</key>
 				<integer>2196</integer>
+
+				<!-- 8 hours -->
 				<key>FeedbackUpdateSeconds</key>
 				<integer>28800</integer>
+
 				<key>Environment</key>
 				<string>PRODUCTION</string>
+
 				<key>EnableStaggering</key>
 				<false/>
+
 				<key>StaggerSeconds</key>
 				<integer>3</integer>
+
 				<key>CalDAV</key>
 				<dict>
+					<key>Enabled</key>
+					<false/>
+
+					<key>CertificatePath</key>
+					<string>Certificates/apns:com.apple.calendar.cert.pem</string>
+
+					<key>PrivateKeyPath</key>
+					<string>Certificates/apns:com.apple.calendar.key.pem</string>
+
 					<key>AuthorityChainPath</key>
 					<string>Certificates/apns:com.apple.calendar.chain.pem</string>
-					<key>CertificatePath</key>
-					<string>Certificates/apns:com.apple.calendar.cert.pem</string>
+
 					<key>Passphrase</key>
 					<string></string>
-					<key>PrivateKeyPath</key>
-					<string>Certificates/apns:com.apple.calendar.key.pem</string>
+
+					<key>KeychainIdentity</key>
+					<string>apns:com.apple.calendar</string>
+
 					<key>Topic</key>
 					<string></string>
 				</dict>
+
 				<key>CardDAV</key>
 				<dict>
+					<key>Enabled</key>
+					<false/>
+
+					<key>CertificatePath</key>
+					<string>Certificates/apns:com.apple.contact.cert.pem</string>
+
+					<key>PrivateKeyPath</key>
+					<string>Certificates/apns:com.apple.contact.key.pem</string>
+
 					<key>AuthorityChainPath</key>
 					<string>Certificates/apns:com.apple.contact.chain.pem</string>
-					<key>CertificatePath</key>
-					<string>Certificates/apns:com.apple.contact.cert.pem</string>
+
 					<key>Passphrase</key>
 					<string></string>
-					<key>PrivateKeyPath</key>
-					<string>Certificates/apns:com.apple.contact.key.pem</string>
+
+					<key>KeychainIdentity</key>
+					<string>apns:com.apple.contact</string>
+
 					<key>Topic</key>
 					<string></string>
 				</dict>
 			</dict>
+
 			<key>AMP</key>
 			<dict>
 				<key>Enabled</key>
 				<false/>
+
 				<key>Port</key>
 				<integer>62311</integer>
+
 				<key>EnableStaggering</key>
 				<false/>
+
 				<key>StaggerSeconds</key>
 				<integer>3</integer>
 			</dict>
@@ -1023,47 +1682,74 @@
 
 	<key>DirectoryProxy</key>
 	<dict>
+		<key>SocketPath</key>
+		<string>directory-proxy.sock</string>
+
 		<key>InProcessCachingSeconds</key>
 		<integer>60</integer>
+
 		<key>InSidecarCachingSeconds</key>
 		<integer>120</integer>
-		<key>SocketPath</key>
-		<string>directory-proxy.sock</string>
 	</dict>
 
 	<!-- Support multiple hosts within a domain -->
+
 	<key>Servers</key>
 	<dict>
+		<!-- Multiple servers enabled or not -->
 		<key>Enabled</key>
 		<false/>
-		<key>ConduitName</key>
-		<string>conduit</string>
+
+		<!-- File path for server information -->
 		<key>ConfigFile</key>
 		<string>localservers.xml</string>
+
+		<!-- Pool size for connections between servers -->
+		<key>MaxClients</key>
+		<integer>5</integer>
+
+		<!-- Name for top-level inbox resource -->
 		<key>InboxName</key>
 		<string>podding</string>
-		<key>MaxClients</key>
-		<integer>5</integer>
+
+		<!-- Name for top-level cross-pod resource -->
+		<key>ConduitName</key>
+		<string>conduit</string>
 	</dict>
 
 	<!-- Performance tuning -->
+
+	<!-- Set the maximum number of outstanding requests to this server. -->
 	<key>MaxRequests</key>
 	<integer>3</integer>
+
 	<key>MaxAccepts</key>
 	<integer>1</integer>
 
+	<!-- The maximum number of outstanding database connections per database
+	     connection pool. When SharedConnectionPool (see above) is set to True,
+	     this is the total number of outgoing database connections allowed to the
+	     entire server; when SharedConnectionPool is False - this is the default -
+	     this is the number of database connections used per worker process. -->
 	<key>MaxDBConnectionsPerPool</key>
 	<integer>10</integer>
 
 	<key>ListenBacklog</key>
 	<integer>2024</integer>
 
+	<!-- Max. time between request lines -->
 	<key>IncomingDataTimeOut</key>
 	<integer>60</integer>
+
+	<!-- Max. time between pipelined requests -->
 	<key>PipelineIdleTimeOut</key>
 	<integer>15</integer>
+
+	<!-- Max. time for response processing -->
 	<key>IdleConnectionTimeOut</key>
 	<integer>360</integer>
+
+	<!-- Max. time for client close -->
 	<key>CloseConnectionTimeOut</key>
 	<integer>15</integer>
 
@@ -1072,59 +1758,81 @@
 
 	<key>MaxMultigetWithDataHrefs</key>
 	<integer>5000</integer>
+
 	<key>MaxQueryWithDataResults</key>
 	<integer>1000</integer>
 
+	<!-- How many results to return for principal-property-search REPORT requests -->
 	<key>MaxPrincipalSearchReportResults</key>
 	<integer>500</integer>
 
 	<!-- Client fixes per user-agent match -->
+
 	<key>ClientFixes</key>
 	<dict>
 		<key>ForceAttendeeTRANSP</key>
 		<array>
-			<string>iOS/8\.0(\..*)?</string>
-			<string>iOS/8\.1(\..*)?</string>
-			<string>iOS/8\.2(\..*)?</string>
+			<string>iOS/8\\.0(\\..*)?</string>
+			<string>iOS/8\\.1(\\..*)?</string>
+			<string>iOS/8\\.2(\\..*)?</string>
 		</array>
 	</dict>
 
 	<!-- Localization -->
+
 	<key>Localization</key>
 	<dict>
+		<key>TranslationsDirectory</key>
+		<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/translations</string>
+
+		<key>LocalesDirectory</key>
+		<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/locales</string>
+
 		<key>Language</key>
 		<string></string>
-		<key>LocalesDirectory</key>
-		<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/locales</string>
-		<key>TranslationsDirectory</key>
-		<string>/Applications/Server.app/Contents/ServerRoot/usr/share/caldavd/share/translations</string>
 	</dict>
 
-	<!-- Implementation details -->
+	<!-- Implementation details
+
+	     The following are specific to how the server is built, and useful for
+	     development, but shouldn't be needed by users. -->
+
+	<!-- Twisted -->
 	<key>Twisted</key>
 	<dict>
 		<key>reactor</key>
 		<string>select</string>
 	</dict>
+
+	<!-- Umask -->
 	<key>umask</key>
 	<integer>18</integer>
 
+	<!-- A TCP port used for communication between the child and master processes
+	     (bound to 127.0.0.1). Specify 0 to let OS assign a port. -->
 	<key>ControlPort</key>
 	<integer>0</integer>
 
+	<!-- A unix socket used for communication between the child and master
+	     processes. If blank, then an AF_INET socket is used instead. -->
 	<key>ControlSocket</key>
 	<string>caldavd.sock</string>
 
+	<!-- Support for Content-Encoding compression options as specified in RFC2616
+	     Section 3.5 Defaults off, because it weakens TLS (CRIME attack). -->
 	<key>ResponseCompression</key>
 	<false/>
 
+	<!-- The retry-after value (in seconds) to return with a 503 error -->
 	<key>HTTPRetryAfter</key>
 	<integer>180</integer>
 
+	<!-- Profiling options -->
 	<key>Profiling</key>
 	<dict>
 		<key>Enabled</key>
 		<false/>
+
 		<key>BaseDirectory</key>
 		<string>/tmp/stats</string>
 	</dict>
@@ -1133,88 +1841,136 @@
 	<dict>
 		<key>MaxClients</key>
 		<integer>5</integer>
+
 		<key>Pools</key>
 		<dict>
 			<key>Default</key>
 			<dict>
+				<!-- A unix socket used for communication with memcached. If MemcacheSocket
+				     is empty string, an AF_INET socket is used. -->
+				<key>MemcacheSocket</key>
+				<string>memcache.sock</string>
+
+				<key>ClientEnabled</key>
+				<true/>
+
+				<key>ServerEnabled</key>
+				<true/>
+
 				<key>BindAddress</key>
 				<string>127.0.0.1</string>
-				<key>ClientEnabled</key>
-				<true/>
+
+				<key>Port</key>
+				<integer>11311</integer>
+
+				<!-- Possible types: "OpenDirectoryBacker", "ImplicitUIDLock",
+				     "RefreshUIDLock", "DIGESTCREDENTIALS", "resourceInfoDB", "pubsubnodes",
+				     "FBCache", "ScheduleAddressMapper", "SQL.props", "SQL.calhome",
+				     "SQL.adbkhome", -->
 				<key>HandleCacheTypes</key>
 				<array>
 					<string>Default</string>
 				</array>
-				<key>MemcacheSocket</key>
-				<string>memcache.sock</string>
-				<key>Port</key>
-				<integer>11311</integer>
-				<key>ServerEnabled</key>
-				<true/>
 			</dict>
+
+			<!-- "Shared": { "ClientEnabled": True, "ServerEnabled": True, "BindAddress":
+			     "127.0.0.1", "Port": 11211, "HandleCacheTypes": [ "ProxyDB",
+			     "DelegatesDB", "PrincipalToken", ] }, -->
 		</dict>
+
+		<!-- Find in PATH -->
 		<key>memcached</key>
 		<string>memcached</string>
+
+		<!-- Megabytes -->
 		<key>MaxMemory</key>
 		<integer>0</integer>
+
 		<key>Options</key>
 		<array>
 		</array>
+
 		<key>ProxyDBKeyNormalization</key>
 		<true/>
 	</dict>
 
 	<key>Postgres</key>
 	<dict>
-		<key>BuffersToConnectionsRatio</key>
-		<real>1.5</real>
+		<key>DatabaseName</key>
+		<string>caldav</string>
+
 		<key>ClusterName</key>
 		<string>cluster</string>
-		<key>Ctl</key>
-		<string>pg_ctl</string>
-		<key>DatabaseName</key>
-		<string>caldav</string>
-		<key>ExtraConnections</key>
-		<integer>3</integer>
-		<key>Init</key>
-		<string>initdb</string>
-		<key>ListenAddresses</key>
-		<array>
-		</array>
+
 		<key>LogFile</key>
 		<string>postgres.log</string>
+
 		<key>LogRotation</key>
 		<false/>
+
+		<key>SocketDirectory</key>
+		<string></string>
+
+		<key>SocketName</key>
+		<string></string>
+
+		<key>ListenAddresses</key>
+		<array>
+		</array>
+
+		<!-- BuffersToConnectionsRatio * MaxConnections Note: don't set this, it will
+		     be computed dynamically See _updateMultiProcess( ) below for details -->
+		<key>SharedBuffers</key>
+		<integer>0</integer>
+
+		<!-- Dynamically computed based on ProcessCount, etc. Note: don't set this, it
+		     will be computed dynamically See _updateMultiProcess( ) below for details -->
 		<key>MaxConnections</key>
 		<integer>0</integer>
+
+		<!-- how many extra connections to leave for utilities -->
+		<key>ExtraConnections</key>
+		<integer>3</integer>
+
+		<key>BuffersToConnectionsRatio</key>
+		<real>1.5</real>
+
 		<key>Options</key>
 		<array>
 			<string>-c standard_conforming_strings=on</string>
 		</array>
-		<key>SharedBuffers</key>
-		<integer>0</integer>
-		<key>SocketDirectory</key>
-		<string></string>
-		<key>SocketName</key>
-		<string></string>
+
+		<!-- If the DBType is '', and we're spawning postgres ourselves, where is the
+		     pg_ctl tool to spawn it with? -->
+		<key>Ctl</key>
+		<string>pg_ctl</string>
+
+		<!-- If the DBType is '', and we're spawning postgres ourselves, where is the
+		     initdb tool to create its database cluster with? -->
+		<key>Init</key>
+		<string>initdb</string>
 	</dict>
 
 	<key>QueryCaching</key>
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
+		<key>MemcachedPool</key>
+		<string>Default</string>
+
 		<key>ExpireSeconds</key>
 		<integer>3600</integer>
-		<key>MemcachedPool</key>
-		<string>Default</string>
 	</dict>
 
 	<key>GroupCaching</key>
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
 		<key>UpdateSeconds</key>
 		<integer>300</integer>
+
 		<key>UseDirectoryBasedDelegates</key>
 		<false/>
 	</dict>
@@ -1223,36 +1979,51 @@
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
+		<key>ReconciliationDelaySeconds</key>
+		<integer>5</integer>
+
+		<!-- 1 hour -->
 		<key>AutoUpdateSecondsFromNow</key>
 		<integer>3600</integer>
-		<key>ReconciliationDelaySeconds</key>
-		<integer>5</integer>
 	</dict>
 
 	<key>AutomaticPurging</key>
 	<dict>
 		<key>Enabled</key>
 		<true/>
+
+		<!-- 7 days -->
+		<key>PollingIntervalSeconds</key>
+		<integer>604800</integer>
+
+		<!-- No staggering -->
 		<key>CheckStaggerSeconds</key>
 		<integer>0</integer>
-		<key>GroupPurgeIntervalSeconds</key>
+
+		<!-- 7 days -->
+		<key>PurgeIntervalSeconds</key>
 		<integer>604800</integer>
+
 		<key>HomePurgeDelaySeconds</key>
 		<integer>60</integer>
-		<key>PollingIntervalSeconds</key>
+
+		<!-- 7 days -->
+		<key>GroupPurgeIntervalSeconds</key>
 		<integer>604800</integer>
-		<key>PurgeIntervalSeconds</key>
-		<integer>604800</integer>
 	</dict>
 
 	<key>Manhole</key>
 	<dict>
 		<key>Enabled</key>
 		<false/>
+
 		<key>StartingPortNumber</key>
 		<integer>5000</integer>
+
 		<key>DPSPortNumber</key>
 		<integer>4999</integer>
+
 		<key>PasswordFilePath</key>
 		<string></string>
 	</dict>
@@ -1262,55 +2033,86 @@
 
 	<key>EnableResponseCache</key>
 	<true/>
+
+	<!-- Minutes -->
 	<key>ResponseCacheTimeout</key>
 	<integer>30</integer>
 
 	<key>EnableFreeBusyCache</key>
 	<true/>
+
 	<key>FreeBusyCacheDaysBack</key>
 	<integer>7</integer>
+
 	<key>FreeBusyCacheDaysForward</key>
 	<integer>84</integer>
 
 	<key>FreeBusyIndexLowerLimitDays</key>
 	<integer>365</integer>
+
 	<key>FreeBusyIndexExpandAheadDays</key>
 	<integer>365</integer>
+
 	<key>FreeBusyIndexExpandMaxDays</key>
 	<integer>1825</integer>
+
 	<key>FreeBusyIndexDelayedExpand</key>
 	<false/>
+
 	<key>FreeBusyIndexSmartUpdate</key>
 	<true/>
 
+	<!-- The RootResource uses a twext property store. Specify the class here -->
 	<key>RootResourcePropStoreClass</key>
 	<string>txweb2.dav.xattrprops.xattrPropertyStore</string>
 
+	<!-- Used in the command line utilities to specify which service class to use
+	     to carry out work. -->
 	<key>UtilityServiceClass</key>
 	<string></string>
 
+	<!-- Inbox items created more than MigratedInboxDaysCutoff days in the past are
+	     removed during migration -->
 	<key>MigratedInboxDaysCutoff</key>
 	<integer>60</integer>
 
+	<!-- The default timezone for the server; on OS X you can leave this empty and
+	     the system's timezone will be used.  If empty and not on OS X it will
+	     default to America/Los_Angeles. -->
 	<key>DefaultTimezone</key>
 	<string></string>
 
+	<!-- After this many seconds of no admin requests, shutdown the agent.  Zero
+	     means no automatic shutdown. -->
 	<key>AgentInactivityTimeoutSeconds</key>
 	<integer>300</integer>
 
+	<!-- Program to execute if the service cannot start; for example in OS X we
+	     want to call serveradmin to disable the service so launchd does not keep
+	     respawning it.  Empty string to disable this feature. -->
 	<key>ServiceDisablingProgram</key>
 	<string></string>
 
+	<!-- Program to execute to post an alert to the administrator; for example in
+	     OS X we want to call calendarserver_alert &lt;alert-type&gt; &lt;args&gt; -->
 	<key>AlertPostingProgram</key>
 	<string></string>
 
+	<!-- These three keys are relative to ConfigRoot: -->
+
+	<!-- Config to read first and merge -->
 	<key>ImportConfig</key>
 	<string></string>
 
+	<!-- Other plists to parse after this one; note that an Include can change the
+	     ServerRoot and/or ConfigRoot, thereby affecting the locations of the
+	     following Includes in the list. (Useful for service directory relocation) -->
 	<key>Includes</key>
 	<array>
 	</array>
 
+	<!-- Which config file calendarserver_config should  write to for changes;
+	     empty string means the main config file -->
 	<key>WritableConfigFile</key>
 	<string></string>
 </dict>

Added: CalendarServer/trunk/twistedcaldav/dumpconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/dumpconfig.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/dumpconfig.py	2015-12-18 20:02:34 UTC (rev 15404)
@@ -0,0 +1,284 @@
+##
+# Copyright (c) 2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from cStringIO import StringIO
+from collections import OrderedDict
+from json.encoder import encode_basestring
+from plistlib import PlistWriter, _escapeAndEncode
+import json
+import os
+import re
+import textwrap
+
+def parseConfigItem(item):
+    """
+    Read the definition of a "DEFAULT_*" value from the stdconfig.py file so that we get
+    the literal Python source as a L{str} that we then then process into JSON.
+
+    @param item: the "DEFAULT_*" item to read
+    @type item: L{str}
+
+    @return: the "DEFAULT_*" value
+    @rtype: L{str}
+    """
+    with open(os.path.join(os.path.dirname(__file__), "stdconfig.py")) as f:
+        # Read up to the first line containing DEFAULT_*
+        while f.readline() != "{} = {{\n".format(item):
+            continue
+
+        # Build list of all lines up to the end of the DEFAULT_* definition and
+        # make it look like a JSON object
+        lines = ['{']
+        line = f.readline()
+        while line != "}\n":
+            lines.append(line[:-1])
+            line = f.readline()
+        lines.append('}')
+
+    return "\n".join(lines)
+
+
+
+def processConfig(configlines, with_comments=False, verbose=False, substitutions=None):
+    """
+    Process the "raw" config lines from stdconfig.py into a JSON object
+    (a Python L{dict}) that is ordered and contains commentary based on
+    the Python comments.
+
+    @param configlines: config data lines
+    @type configlines: L{list} of L{str}
+    @param with_comments: whether to include comments or not
+    @type with_comments: L{bool}
+    @param verbose: print out intermediate state
+    @type verbose: L{bool}
+
+    @return: the serialized JSON object
+    @rtype: L{OrderedDict}
+    """
+
+    # Comments will either be "block" (as in section dividers) or "inline"
+    # (as in appended to the end of the line). We treat these slightly
+    # differently wrt to whitespace and where they appear.
+    lines = []
+    ctr = 0
+    block_comment = []
+    inline_comment = []
+
+    # Regular expression to match an inline comment and a
+    # value containing a numeric expression that needs to be
+    # evaluated (e.g. "60 * 60")
+    comments = re.compile("([ ]*.*?,?)[ ]*#[ ]*(.*)[ ]*$")
+    value = re.compile("([^:]+:[ ]+)([0-9 \*]+)(.*)")
+
+    for line in configlines.splitlines():
+
+        if line.strip() and line.strip()[0] == "#":
+            # Line with just a comment is a block comment unless the
+            # previous comment was inline (in which case it is a multi-line
+            # inline). Aggregate block and inline comments into one overall
+            # comment.
+            comment = line.strip()[1:].strip()
+            if len(comment) == 0 and len(block_comment) == 0 and len(inline_comment) == 0:
+                pass
+            elif inline_comment:
+                inline_comment.append(comment if comment else "\n")
+            else:
+                block_comment.append(comment if comment else "\n")
+            continue
+        elif block_comment:
+            # Generate a block comment JSON member
+            if with_comments:
+                comment_type = "comment_" if line.strip() and block_comment[-1] != "\n" else "section_"
+                while block_comment[-1] == "\n":
+                    block_comment.pop()
+                lines.append("\"{}{}\": {},".format(comment_type, ctr, encode_basestring(" ".join(block_comment))))
+            ctr += 1
+            block_comment = []
+        elif inline_comment:
+            # Generate an inline comment JSON member
+            if with_comments:
+                lines.insert(-1, "\"comment_{}\": {},".format(ctr, encode_basestring(" ".join(inline_comment))))
+            ctr += 1
+            inline_comment = []
+
+        # Check if the current line contains an inline comment, if so extract
+        # the comment and add to the current inline comments list
+        m = comments.match(line)
+        if m:
+            inline_comment.append(m.group(2))
+            append = m.group(1)
+        else:
+            append = line
+
+        # Do some simple value conversions
+        append = append.rstrip().replace(" None", ' ""').replace(" True", " true").replace(" False", " false").replace("\\", "\\\\")
+
+        # Look for special substitutions
+        if substitutions:
+            for subskey in substitutions.keys():
+                pos = append.find(subskey)
+                if pos >= 0:
+                    actual = append[pos + len(subskey) + 2:]
+                    comma = ""
+                    if actual[-1] == ",":
+                        actual = actual[:-1]
+                        comma = ","
+                    actual = actual[:-2]
+                    append = "{}{}{}".format(
+                        append[:pos],
+                        json.dumps(substitutions[subskey][actual]),
+                        comma,
+                    )
+                    break
+
+
+        # Look for numeric expressions in the value and eval() those to get a value
+        # that is compatible with JSON
+        m = value.match(append)
+        if m:
+            expression = eval(m.group(2))
+            append = "{}{}{}".format(m.group(1), expression, m.group(3))
+
+        # Remove trailing commas for the last items in an array
+        # or object as JSON does not like that
+        if append.strip() and append.strip()[0] in ("]", "}"):
+            if lines[-1][-1] == ",":
+                lines[-1] = lines[-1][:-1]
+
+        # Line is ready to use
+        lines.append(append)
+
+    newj = "\n".join(lines)
+    if verbose:
+        print(newj)
+
+    # Created an ordered JSON object
+    j = json.loads(newj, object_pairs_hook=OrderedDict)
+    return j
+
+
+
+class OrderedPlistWriter(PlistWriter):
+    """
+    L{PlistWriter} that maintains the order of dict items. It also handles special keys
+    "section_" and "comment_" which are used to insert XML comments in the plist output.
+    Some additional blank lines are also added for readability of the plist.
+    """
+
+    def writeDict(self, d):
+        """
+        Basically a copy of L{PlistWriter.writeDict} that does not sort the dict keys
+        if the dict type is L{OrderedDict}.
+        """
+        self.beginElement("dict")
+        items = d.items()
+        if not isinstance(d, OrderedDict):
+            items.sort()
+        newline = False
+        for key, value in items:
+            if not isinstance(key, (str, unicode)):
+                raise TypeError("keys must be strings")
+            if newline:
+                self.writeln("")
+            if key.startswith("section_"):
+                self.writeComment(value)
+                newline = True
+            elif key.startswith("comment_"):
+                self.writeComment(value)
+                newline = False
+            else:
+                self.simpleElement("key", key)
+                self.writeValue(value)
+                newline = True
+        self.endElement("dict")
+
+
+    def writeComment(self, comment):
+
+        indent = self.indentLevel * self.indent
+
+        paragraphs = comment.splitlines()
+        for ctr, paragraph in enumerate(comment.splitlines()):
+            line = _escapeAndEncode(paragraph).strip()
+            initial_indent = ("{}<!-- " if ctr == 0 else "{}     ").format(indent)
+            subsequent_indent = "{}     ".format(indent)
+
+            line = textwrap.fill(
+                line,
+                width=80,
+                initial_indent=initial_indent,
+                subsequent_indent=subsequent_indent,
+            )
+
+            if ctr == len(paragraphs) - 1:
+                self.file.write("{} -->\n".format(line))
+            else:
+                self.file.write("{}\n\n".format(line))
+
+
+
+def writeOrderedPlist(rootObject, pathOrFile):
+    """
+    A copy of L{plistlib.writePlist} that uses an L{OrderedPlistWriter} to
+    write the plist.
+    """
+
+    """Write 'rootObject' to a .plist file. 'pathOrFile' may either be a
+    file name or a (writable) file object.
+    """
+    didOpen = 0
+    if isinstance(pathOrFile, (str, unicode)):
+        pathOrFile = open(pathOrFile, "w")
+        didOpen = 1
+    writer = OrderedPlistWriter(pathOrFile)
+    writer.writeln("<plist version=\"1.0\">")
+    writer.writeValue(rootObject)
+    writer.writeln("</plist>")
+    if didOpen:
+        pathOrFile.close()
+
+
+
+def writeOrderedPlistToString(rootObject):
+    """
+    A copy of L{plistlib.writePlistToString} that uses an L{writeOrderedPlist} to
+    write the plist.
+    """
+
+    """Return 'rootObject' as a plist-formatted string.
+    """
+    f = StringIO()
+    writeOrderedPlist(rootObject, f)
+    return f.getvalue()
+
+if __name__ == '__main__':
+
+    # Generate a set of serialized JSON objects for the *_PARAMS config items
+    maps = {
+        "DEFAULT_SERVICE_PARAMS": "",
+        "DEFAULT_RESOURCE_PARAMS": "",
+        "DEFAULT_AUGMENT_PARAMS": "",
+        "DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS": "",
+    }
+
+    for item in maps.keys():
+        lines = parseConfigItem(item)
+        maps[item] = processConfig(lines, with_comments=True, verbose=False)
+
+    # Generate the plist for the default config, substituting for the *_PARAMS items
+    lines = parseConfigItem("DEFAULT_CONFIG")
+    j = processConfig(lines, with_comments=True, verbose=False, substitutions=maps)
+    print(writeOrderedPlistToString(j))

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2015-12-17 20:18:32 UTC (rev 15403)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2015-12-18 20:02:34 UTC (rev 15404)
@@ -57,15 +57,15 @@
 
 DEFAULT_SERVICE_PARAMS = {
     "xml": {
-        "recordTypes": ("users", "groups"),
+        "recordTypes": ["users", "groups"],
         "xmlFile": "accounts.xml",
     },
     "opendirectory": {
-        "recordTypes": ("users", "groups"),
+        "recordTypes": ["users", "groups"],
         "node": "/Search",
     },
     "ldap": {
-        "recordTypes": ("users", "groups"),
+        "recordTypes": ["users", "groups"],
         "uri": "ldap://localhost/",
         "credentials": {
             "dn": None,
@@ -80,17 +80,17 @@
             "addresses": "cn=addresses",
         },
         "mapping": {
-            "uid": ["apple-generateduid", ],
-            "guid": ["apple-generateduid", ],
-            "shortNames": ["uid", ],
-            "fullNames": ["cn", ],
-            "emailAddresses": ["mail", ],
-            "memberDNs": ["uniqueMember", ],
+            "uid": ["apple-generateduid"],
+            "guid": ["apple-generateduid"],
+            "shortNames": ["uid"],
+            "fullNames": ["cn"],
+            "emailAddresses": ["mail"],
+            "memberDNs": ["uniqueMember"],
             "hasCalendars": [],
             "autoScheduleMode": [],
             "autoAcceptGroup": [],
-            "readWriteProxy": ["icsContact", ],
-            "readOnlyProxy": ["icsSecondaryOwners", ],
+            "readWriteProxy": ["icsContact"],
+            "readOnlyProxy": ["icsSecondaryOwners"],
             "serviceNodeUID": [],
         },
         "extraFilters": {
@@ -106,18 +106,18 @@
 
 DEFAULT_RESOURCE_PARAMS = {
     "xml": {
-        "recordTypes": ("locations", "resources", "addresses"),
+        "recordTypes": ["locations", "resources", "addresses"],
         "xmlFile": "resources.xml",
     },
     "opendirectory": {
-        "recordTypes": ("locations", "resources", "addresses"),
+        "recordTypes": ["locations", "resources", "addresses"],
         "node": "/Search",
     },
 }
 
 DEFAULT_AUGMENT_PARAMS = {
     "twistedcaldav.directory.augment.AugmentXMLDB": {
-        "xmlFiles": ["augments.xml", ],
+        "xmlFiles": ["augments.xml"],
         "statSeconds": 15,
     },
     "twistedcaldav.directory.augment.AugmentSqliteDB": {
@@ -132,7 +132,7 @@
 }
 
 
-directoryAddressBookBackingServiceDefaultParams = {
+DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS = {
     "twistedcaldav.directory.xmlfile.XMLDirectoryService": {
         "xmlFile": "/etc/carddavd/accounts.xml",
     },
@@ -157,9 +157,8 @@
     },
 }
 
+# Note: Don't use None values below; that confuses the command-line parser.
 DEFAULT_CONFIG = {
-    # Note: Don't use None values below; that confuses the command-line parser.
-
     #
     # Public network address information
     #
@@ -169,6 +168,7 @@
     #    default.  For example, it may be the address of a load balancer or
     #    proxy which forwards connections to the server.
     #
+
     "ServerHostName": "", # Network host name.
     "HTTPPort": 0, # HTTP port (0 to disable HTTP)
     "SSLPort": 0, # SSL port (0 to disable HTTPS)
@@ -177,12 +177,11 @@
     "SSLMethod": "SSLv23_METHOD", # SSLv2_METHOD, SSLv3_METHOD, SSLv23_METHOD, TLSv1_METHOD
     "SSLCiphers": "RC4-SHA:HIGH:!ADH",
 
-    # Max-age value for Strict-Transport-Security header; set to 0 to
-    # disable header.
+    # Max-age value for Strict-Transport-Security header; set to 0 to disable header.
     "StrictTransportSecuritySeconds": 7 * 24 * 60 * 60,
 
     #
-    # Network address configuration information
+    # Network address configuration information.
     #
     #    This configures the actual network address that the server binds to.
     #
@@ -200,17 +199,12 @@
     "BindAddresses": [], # List of IP addresses to bind to [empty = all]
     "BindHTTPPorts": [], # List of port numbers to bind to for HTTP
                          # [empty = same as "Port"]
-    "BindSSLPorts": [], # List of port numbers to bind to for SSL
-                        # [empty = same as "SSLPort"]
-    "InheritFDs": [], # File descriptors to inherit for HTTP requests
-                      # (empty = don't inherit)
-    "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests
-                         # (empty = don't inherit)
-    "MetaFD": 0, # Inherited file descriptor to call recvmsg() on to
-                 # receive sockets (none = don't inherit)
+    "BindSSLPorts": [], # List of port numbers to bind to for SSL [empty = same as "SSLPort"]
+    "InheritFDs": [], # File descriptors to inherit for HTTP requests [empty = don't inherit]
+    "InheritSSLFDs": [], # File descriptors to inherit for HTTPS requests [empty = don't inherit]
+    "MetaFD": 0, # Inherited file descriptor to call recvmsg() on to receive sockets (none = don't inherit)
 
-    "UseMetaFD": True, # Use a 'meta' FD, i.e. an FD to transmit other FDs
-                       # to slave processes.
+    "UseMetaFD": True, # Use a 'meta' FD, i.e. an FD to transmit other FDs to slave processes.
 
     "UseDatabase": True, # True: database; False: files
 
@@ -261,11 +255,13 @@
     #
     # Work queue configuration information
     #
+
     "WorkQueue": {
         "queuePollInterval": 0.1,   # Interval in seconds for job queue polling
         "queueOverdueTimeout": 300, # Number of seconds before an assigned job is considered overdue
         "queuePollingBackoff": [     # Array of array that describe the threshold and new polling interval
-            [60, 60], [5, 1]         # for job queue polling back off
+                                     # for job queue polling back off
+            [60, 60], [5, 1]
         ],
 
         "overloadLevel": 95,        # Queue capacity (percentage) which causes job processing to halt
@@ -426,7 +422,7 @@
     "AccessLogFile"  : "access.log", # Apache-style access log
     "ErrorLogFile"   : "error.log", # Server activity log
     "AgentLogFile"   : "agent.log", # Agent activity log
-    "UtilityLogFile"   : "{}.log".format(basename(sys.argv[0])), # Command line utility log
+    "UtilityLogFile" : "utility.log", # Utility log - name will be dynamically changed to executable name
     "ErrorLogEnabled"   : True, # True = use log file, False = stdout
     "ErrorLogRotateMB"  : 10, # Rotate error log after so many megabytes
     "ErrorLogMaxRotatedFiles"  : 5, # Retain this many error log files
@@ -480,11 +476,11 @@
     # Process management
     #
 
-    # Username and Groupname to drop privileges to, if empty privileges will
-    # not be dropped.
-
+    # Username and Groupname to drop privileges to, if empty privileges will not be dropped.
     "UserName": "",
     "GroupName": "",
+
+    # Multi-process
     "ProcessType": "Combined",
     "MultiProcess": {
         "ProcessCount": 0,
@@ -502,14 +498,10 @@
         "Enabled" : True,
         "Seconds" : 60, # How often to check memory sizes (in seconds)
         "Bytes"   : 2 * 1024 * 1024 * 1024, # Memory limit (RSS in bytes)
-        "ResidentOnly" : True,  # True: only take into account resident memory;
-                                # False: include virtual memory
+        "ResidentOnly" : True,  # True: only take into account resident memory; False: include virtual memory
     },
 
-    #
-    # Service ACLs
-    #
-    "EnableSACLs": False,
+    "EnableSACLs": False, # Service ACLs
 
     "EnableReadOnlyServer": False, # Make all data read-only
 
@@ -616,17 +608,16 @@
                                                   # If on, it will also cause new accounts to provision with separate
                                                   # calendars for events and tasks.
 
-    "SupportedComponents" : [                      # Set of supported iCalendar components
+    "SupportedComponents" : [                     # Set of supported iCalendar components
         "VEVENT",
         "VTODO",
-        # "VPOLL",
     ],
 
     "EnableTrashCollection": False,  # Enable Trash Collection
     "ExposeTrashCollection": False,  # Expose Trash Collection as a resource
 
     "ParallelUpgrades": False, # Perform upgrades - currently only the
-                               # database -> filesystem migration - but in
+                               # database to filesystem migration - but in
                                # the future, hopefully all relevant
                                # upgrades - in parallel in subprocesses.
 
@@ -670,7 +661,7 @@
     "DirectoryAddressBook": {
         "Enabled": True,
         "type": "twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService",
-        "params": directoryAddressBookBackingServiceDefaultParams["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
+        "params": DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS["twistedcaldav.directory.opendirectorybacker.OpenDirectoryBackingService"],
         "name": "directory",
         "MaxQueryResults": 1000,
     },
@@ -679,14 +670,10 @@
 
     # /XXX CardDAV
 
-    #
     # Web-based administration
-    #
     "EnableWebAdmin"          : True,
 
-    #
     # JSON control API - only for testing
-    #
     "EnableControlAPI"        : False,
 
     #
@@ -974,8 +961,7 @@
     # processes. If blank, then an AF_INET socket is used instead.
     "ControlSocket": "caldavd.sock",
 
-    # Support for Content-Encoding compression options as specified in
-    # RFC2616 Section 3.5
+    # Support for Content-Encoding compression options as specified in RFC2616 Section 3.5
     # Defaults off, because it weakens TLS (CRIME attack).
     "ResponseCompression": False,
 
@@ -999,8 +985,7 @@
                 "ServerEnabled": True,
                 "BindAddress": "127.0.0.1",
                 "Port": 11311,
-                "HandleCacheTypes": [
-                    "Default",
+                "HandleCacheTypes": [ # Possible types:
                     # "OpenDirectoryBacker",
                     # "ImplicitUIDLock",
                     # "RefreshUIDLock",
@@ -1012,6 +997,7 @@
                     # "SQL.props",
                     # "SQL.calhome",
                     # "SQL.adbkhome",
+                    "Default",
                 ]
             },
             # "Shared": {
@@ -1051,9 +1037,9 @@
         "Options": [
             "-c standard_conforming_strings=on",
         ],
-        "Ctl": "pg_ctl", # Iff the DBType is '', and we're spawning postgres
+        "Ctl": "pg_ctl", # If the DBType is '', and we're spawning postgres
                          # ourselves, where is the pg_ctl tool to spawn it with?
-        "Init": "initdb", # Iff the DBType is '', and we're spawning postgres
+        "Init": "initdb", # If the DBType is '', and we're spawning postgres
                           # ourselves, where is the initdb tool to create its
                           # database cluster with?
     },
@@ -1110,12 +1096,10 @@
     # The RootResource uses a twext property store. Specify the class here
     "RootResourcePropStoreClass": "txweb2.dav.xattrprops.xattrPropertyStore",
 
-    # Used in the command line utilities to specify which service class to
-    # use to carry out work.
+    # Used in the command line utilities to specify which service class to use to carry out work.
     "UtilityServiceClass": "",
 
-    # Inbox items created more than MigratedInboxDaysCutoff days in the past are removed
-    # during migration
+    # Inbox items created more than MigratedInboxDaysCutoff days in the past are removed during migration
     "MigratedInboxDaysCutoff": 60,
 
     # The default timezone for the server; on OS X you can leave this empty and the
@@ -1496,19 +1480,19 @@
             newParams = items["DirectoryAddressBook"].get("params", {})
             mergeData(oldParams, newParams)
         else:
-            if dsType in directoryAddressBookBackingServiceDefaultParams:
-                configDict.DirectoryAddressBook.params = copy.deepcopy(directoryAddressBookBackingServiceDefaultParams[dsType])
+            if dsType in DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS:
+                configDict.DirectoryAddressBook.params = copy.deepcopy(DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS[dsType])
             else:
                 configDict.DirectoryAddressBook.params = {}
 
     for param in items.get("DirectoryAddressBook", {}).get("params", {}):
-        if param not in directoryAddressBookBackingServiceDefaultParams[dsType]:
+        if param not in DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS[dsType]:
             raise ConfigurationError("Parameter %s is not supported by service %s" % (param, dsType))
 
     mergeData(configDict, items)
 
     for param in tuple(configDict.DirectoryAddressBook.params):
-        if param not in directoryAddressBookBackingServiceDefaultParams[configDict.DirectoryAddressBook.type]:
+        if param not in DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS[configDict.DirectoryAddressBook.type]:
             del configDict.DirectoryAddressBook.params[param]
 
 
@@ -1642,6 +1626,11 @@
 
 
 
+def _updateUtilityLog(configDict, reloading=False):
+    configDict["UtilityLogFile"] = "{}.log".format(basename(sys.argv[0])), # Command line utility log
+
+
+
 def _updateLogLevels(configDict, reloading=False):
     log.levels().clearLogLevels()
 
@@ -1787,12 +1776,12 @@
     _updateACLs,
     _updateRejectClients,
     _updateClientFixes,
+    _updateUtilityLog,
     _updateLogLevels,
     _updateNotifications,
     _updateICalendar,
     _updateScheduling,
     _updateSharing,
-    # _updateServers,
     _updateCompliance,
 )
 

Added: CalendarServer/trunk/twistedcaldav/test/test_dumpconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_dumpconfig.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_dumpconfig.py	2015-12-18 20:02:34 UTC (rev 15404)
@@ -0,0 +1,135 @@
+##
+# Copyright (c) 2015 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from unittest.case import TestCase
+from twistedcaldav.dumpconfig import parseConfigItem, processConfig, \
+    writeOrderedPlistToString
+from collections import OrderedDict
+
+class TestDumpConfig (TestCase):
+
+    def test_parseConfigItem(self):
+        """
+        Make sure L{parseConfigItem} can parse the DEFAULT_* items
+        """
+
+        items = {
+            "DEFAULT_SERVICE_PARAMS",
+            "DEFAULT_RESOURCE_PARAMS",
+            "DEFAULT_AUGMENT_PARAMS",
+            "DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS",
+            "DEFAULT_CONFIG",
+        }
+
+        for item in items:
+            lines = parseConfigItem(item)
+            self.assertNotEqual(len(lines), 0, msg="Failed {}".format(item))
+
+
+    def test_writeOrderedPlistToString(self):
+        """
+        Make sure L{writeOrderedPlistToString} preserves key order
+        """
+
+        data = OrderedDict()
+        data["KeyB"] = "1"
+        data["KeyA"] = "2"
+        data["KeyC"] = "3"
+        data["KeyE"] = "4"
+        data["KeyD"] = "5"
+
+        plist = writeOrderedPlistToString(data)
+        self.assertEqual(plist, """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+\t<key>KeyB</key>
+\t<string>1</string>
+
+\t<key>KeyA</key>
+\t<string>2</string>
+
+\t<key>KeyC</key>
+\t<string>3</string>
+
+\t<key>KeyE</key>
+\t<string>4</string>
+
+\t<key>KeyD</key>
+\t<string>5</string>
+</dict>
+</plist>
+""")
+
+
+    def test_plistWithComments(self):
+        """
+        Make sure L{writeOrderedPlistToString} preserves key order
+        """
+
+        data = OrderedDict()
+        data["comment_1"] = "All about KeyB"
+        data["KeyB"] = "1"
+        data["section_2"] = "Details on KeyA & KeyC"
+        data["comment_3"] = "All about KeyA"
+        data["KeyA"] = "2"
+        data["comment_4"] = "All about KeyC"
+        data["KeyC"] = "3"
+
+        plist = writeOrderedPlistToString(data)
+        self.assertEqual(plist, """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+\t<!-- All about KeyB -->
+\t<key>KeyB</key>
+\t<string>1</string>
+
+\t<!-- Details on KeyA &amp; KeyC -->
+
+\t<!-- All about KeyA -->
+\t<key>KeyA</key>
+\t<string>2</string>
+
+\t<!-- All about KeyC -->
+\t<key>KeyC</key>
+\t<string>3</string>
+</dict>
+</plist>
+""")
+
+
+    def test_fullPlistDump(self):
+        """
+        Make sure a full dump of DEFAULT_CONFIG works
+        """
+
+        maps = {
+            "DEFAULT_SERVICE_PARAMS": "",
+            "DEFAULT_RESOURCE_PARAMS": "",
+            "DEFAULT_AUGMENT_PARAMS": "",
+            "DEFAULT_DIRECTORY_ADDRESSBOOK_PARAMS": "",
+        }
+
+        for item in maps.keys():
+            lines = parseConfigItem(item)
+            maps[item] = processConfig(lines, with_comments=True, verbose=False)
+
+        # Generate the plist for the default config, substituting for the *_PARAMS items
+        lines = parseConfigItem("DEFAULT_CONFIG")
+        j = processConfig(lines, with_comments=True, verbose=False, substitutions=maps)
+        result = writeOrderedPlistToString(j)
+        self.assertIn('<plist version="1.0">', result)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20151218/3fa312bc/attachment-0001.html>


More information about the calendarserver-changes mailing list