mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-04-16 10:48:50 +00:00
Merge to 2.4.2
This commit is contained in:
@@ -2153,7 +2153,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// Cache-entry still exists but folder doesn't exists or synchronize flag is not set.
|
||||
// We ignore the folder and wait for foldersync to do the cleanup.
|
||||
if (!(mfCollection && [mfCollection synchronize]))
|
||||
if (!(mfCollection && [(SOGoGCSFolder*)mfCollection synchronize]))
|
||||
{
|
||||
if (debugOn)
|
||||
[self logWithFormat: @"EAS - Folder %@ not found. Ignoring ...", folderName];
|
||||
@@ -2187,7 +2187,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
realCollectionId = [folderName realCollectionIdWithFolderType: &mergedFolderType];
|
||||
mfCollection = [self collectionFromId: realCollectionId type: mergedFolderType];
|
||||
|
||||
if (!(mfCollection && [mfCollection synchronize]))
|
||||
if (!(mfCollection && [(SOGoGCSFolder*)mfCollection synchronize]))
|
||||
{
|
||||
if (debugOn)
|
||||
[self logWithFormat: @"EAS - Folder %@ not found. Reset personal folder to cleanup", folderName];
|
||||
|
||||
@@ -969,7 +969,7 @@ void handle_eas_terminate(int signum)
|
||||
}
|
||||
|
||||
// Remove the folder from device if it doesn't exist, or don't want to sync it.
|
||||
if (!currentFolder || !([currentFolder synchronize]))
|
||||
if (!currentFolder || !([(SOGoGCSFolder*)currentFolder synchronize]))
|
||||
{
|
||||
// Don't send a delete when MergedFoler is set, we have done it above.
|
||||
// Windows Phones don't like when a <Delete>-folder is sent twice.
|
||||
@@ -1124,7 +1124,7 @@ void handle_eas_terminate(int signum)
|
||||
continue;
|
||||
|
||||
if (![currentFolder isKindOfClass: [SOGoGCSFolder class]] ||
|
||||
![currentFolder synchronize])
|
||||
![(SOGoGCSFolder*)currentFolder synchronize])
|
||||
{
|
||||
[folders removeObjectAtIndex: count];
|
||||
continue;
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [2.4.2](https://github.com/inverse-inc/sogo/compare/SOGo-2.4.1...SOGo-2.4.2) (2022-04-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **calendar(dav):** add DAV:status to DAV:response only when deleted ([9bffee2](https://github.com/inverse-inc/sogo/commit/9bffee269d0479927d32bb8371c732509f062d88)), closes [#5163](https://www.sogo.nu/bugs/view.php?id=5163)
|
||||
* **calendar(dav):** add method attribute to content-type of iTIP reply ([3e96d68](https://github.com/inverse-inc/sogo/commit/3e96d68f308d59a5497807e29dfc8d7df8f2393b)), closes [#5320](https://www.sogo.nu/bugs/view.php?id=5320)
|
||||
* **core:** add security flags to cookies (HttpOnly, secure) ([0f3d7dc](https://github.com/inverse-inc/sogo/commit/0f3d7dc6bcb9457e91c93e89def0310e63e81f3d)), closes [#4525](https://www.sogo.nu/bugs/view.php?id=4525)
|
||||
* **core:** fix GCC 10 compatibility ([dc4fdb2](https://github.com/inverse-inc/sogo/commit/dc4fdb2d5a7c354f72b21ecfbfde306dd2c9c532)), closes [#5029](https://www.sogo.nu/bugs/view.php?id=5029)
|
||||
* **core:** only escape "%" with the SQL LIKE operator ([2389e44](https://github.com/inverse-inc/sogo/commit/2389e4451376862db3b54e2319716615177ea063))
|
||||
* **eas:** gcc v10 compat fixes (fixes [#5029](https://www.sogo.nu/bugs/view.php?id=5029)) ([3d2e5ad](https://github.com/inverse-inc/sogo/commit/3d2e5adee83a87ba5141cc329d8a0ad25a9c2b73))
|
||||
* **mail(css):** restrict the viewport of the message body viewer ([e528096](https://github.com/inverse-inc/sogo/commit/e528096b10f2702e5c948a8d1e53169ce65a1466))
|
||||
* **mail(html):** ban "javascript:" prefix in href, action and formaction ([dd7dd49](https://github.com/inverse-inc/sogo/commit/dd7dd49641fcf24f58c2edd7ca84f7e061cb32d9))
|
||||
* **mail(js):** ban all "on*" events attributes from HTML tags ([f38eded](https://github.com/inverse-inc/sogo/commit/f38eded701fff37f570c91a2c5c62075a0ebe439))
|
||||
* **mail:** don't allow XML inline attachments ([3c85dbd](https://github.com/inverse-inc/sogo/commit/3c85dbd74dc845dae8d5e992fb673575e171074b))
|
||||
|
||||
## [2.4.1](https://github.com/inverse-inc/sogo/compare/SOGo-2.4.0...SOGo-2.4.1) (2021-06-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
# SOGOSPOOL must match the value of the configuration parameter SOGoMailSpoolPath
|
||||
SOGOSPOOL=/var/spool/sogo
|
||||
|
||||
/usr/sbin/tmpwatch 24 "$SOGOSPOOL"
|
||||
find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null
|
||||
find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2007-2014 Inverse inc.
|
||||
Copyright (C) 2007-2022 Inverse inc.
|
||||
Copyright (C) 2004-2005 SKYRIX Software AG
|
||||
|
||||
This file is part of SOGo.
|
||||
@@ -795,7 +795,7 @@ static Class iCalEventK = nil;
|
||||
if ([title length])
|
||||
[baseWhere
|
||||
addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'",
|
||||
[title asSafeSQLString]]];
|
||||
[title asSafeSQLLikeString]]];
|
||||
|
||||
if (component)
|
||||
{
|
||||
@@ -1532,14 +1532,14 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir
|
||||
if ([filters isEqualToString:@"title_Category_Location"] || [filters isEqualToString:@"entireContent"])
|
||||
{
|
||||
[baseWhere addObject: [NSString stringWithFormat: @"(c_title isCaseInsensitiveLike: '%%%@%%' OR c_category isCaseInsensitiveLike: '%%%@%%' OR c_location isCaseInsensitiveLike: '%%%@%%')",
|
||||
[title asSafeSQLString],
|
||||
[title asSafeSQLString],
|
||||
[title asSafeSQLString]]];
|
||||
[title asSafeSQLLikeString],
|
||||
[title asSafeSQLLikeString],
|
||||
[title asSafeSQLLikeString]]];
|
||||
}
|
||||
}
|
||||
else
|
||||
[baseWhere addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'",
|
||||
[title asSafeSQLString]]];
|
||||
[title asSafeSQLLikeString]]];
|
||||
}
|
||||
|
||||
/* prepare mandatory fields */
|
||||
|
||||
@@ -2065,9 +2065,8 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent
|
||||
//
|
||||
else if (scheduling && [event userIsAttendee: ownerUser])
|
||||
{
|
||||
[self sendIMIPReplyForEvent: event
|
||||
from: ownerUser
|
||||
to: [event organizer]];
|
||||
[self sendResponseToOrganizer: event
|
||||
from: ownerUser];
|
||||
}
|
||||
|
||||
[self sendReceiptEmailForObject: event
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2006-2013 Inverse inc.
|
||||
Copyright (C) 2006-2022 Inverse inc.
|
||||
Copyright (C) 2004-2005 SKYRIX Software AG
|
||||
|
||||
This file is part of SOGo.
|
||||
@@ -178,7 +178,7 @@ static NSArray *folderListingFields = nil;
|
||||
|
||||
if ([filter length] > 0)
|
||||
{
|
||||
filter = [filter asSafeSQLString];
|
||||
filter = [filter asSafeSQLLikeString];
|
||||
if ([criteria isEqualToString: @"name_or_address"])
|
||||
qs = [NSString stringWithFormat:
|
||||
@"(c_sn isCaseInsensitiveLike: '%%%@%%') OR "
|
||||
@@ -281,7 +281,7 @@ static NSArray *folderListingFields = nil;
|
||||
if (aName && [aName length] > 0)
|
||||
{
|
||||
aName = [aName asSafeSQLString];
|
||||
qs = [NSString stringWithFormat: @"(c_name='%@')", aName];
|
||||
qs = [NSString stringWithFormat: @"(c_name = '%@')", aName];
|
||||
qualifier = [EOQualifier qualifierWithQualifierFormat: qs];
|
||||
dbRecords = [[self ocsFolder] fetchFields: folderListingFields
|
||||
matchingQualifier: qualifier];
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@class NGImap4Envelope;
|
||||
@class NGImap4EnvelopeAddress;
|
||||
|
||||
NSArray *SOGoMailCoreInfoKeys;
|
||||
extern NSArray *SOGoMailCoreInfoKeys;
|
||||
|
||||
@interface SOGoMailObject : SOGoMailBaseObject
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* NSString+Utilities.h - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2006-2015 Inverse inc.
|
||||
* Copyright (C) 2006-2022 Inverse inc.
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -51,6 +51,7 @@
|
||||
|
||||
/* SQL safety */
|
||||
- (NSString *) asSafeSQLString;
|
||||
- (NSString *) asSafeSQLLikeString;
|
||||
|
||||
/* Unicode safety */
|
||||
- (NSString *) safeString;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* NSString+Utilities.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2006-2015 Inverse inc.
|
||||
* Copyright (C) 2006-2022 Inverse inc.
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -688,9 +688,13 @@ static int cssEscapingCount;
|
||||
|
||||
- (NSString *) asSafeSQLString
|
||||
{
|
||||
return [[[self stringByReplacingString: @"\\" withString: @"\\\\"]
|
||||
stringByReplacingString: @"'" withString: @"\\'"]
|
||||
stringByReplacingString: @"\%" withString: @"\\%"];
|
||||
return [[self stringByReplacingString: @"\\" withString: @"\\\\"]
|
||||
stringByReplacingString: @"'" withString: @"\\'"];
|
||||
}
|
||||
|
||||
- (NSString *) asSafeSQLLikeString
|
||||
{
|
||||
return [[self asSafeSQLString] stringByReplacingString: @"\%" withString: @"\\%"];
|
||||
}
|
||||
|
||||
- (NSUInteger) countOccurrencesOfString: (NSString *) substring
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* SOGoGCSFolder.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2004-2005 SKYRIX Software AG
|
||||
* Copyright (C) 2006-2014 Inverse inc.
|
||||
* Copyright (C) 2006-2022 Inverse inc.
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -1369,42 +1369,19 @@ static NSArray *childRecordFields = nil;
|
||||
withToken: (int) syncToken
|
||||
andBaseURL: (NSString *) baseURL
|
||||
{
|
||||
static NSString *status[] = { @"HTTP/1.1 404 Not Found",
|
||||
@"HTTP/1.1 201 Created",
|
||||
@"HTTP/1.1 200 OK" };
|
||||
NSMutableArray *children;
|
||||
NSString *href;
|
||||
unsigned int statusIndex;
|
||||
|
||||
children = [NSMutableArray arrayWithCapacity: 3];
|
||||
href = [NSString stringWithFormat: @"%@%@",
|
||||
baseURL, [record objectForKey: @"c_name"]];
|
||||
[children addObject: davElementWithContent (@"href", XMLNS_WEBDAV,
|
||||
href)];
|
||||
if (syncToken)
|
||||
{
|
||||
if ([[record objectForKey: @"c_deleted"] intValue] > 0)
|
||||
statusIndex = 0;
|
||||
else
|
||||
{
|
||||
if ([[record objectForKey: @"c_creationdate"] intValue]
|
||||
>= syncToken)
|
||||
statusIndex = 1;
|
||||
else
|
||||
statusIndex = 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
statusIndex = 1;
|
||||
href = [NSString stringWithFormat: @"%@%@", baseURL, [record objectForKey: @"c_name"]];
|
||||
[children addObject: davElementWithContent (@"href", XMLNS_WEBDAV, href)];
|
||||
|
||||
// NSLog (@"webdav sync: %@ (%@)", href, status[statusIndex]);
|
||||
[children addObject: davElementWithContent (@"status", XMLNS_WEBDAV,
|
||||
status[statusIndex])];
|
||||
if (statusIndex)
|
||||
[children
|
||||
addObjectsFromArray: [self _davPropstatsWithProperties: properties
|
||||
andMethodSelectors: selectors
|
||||
fromRecord: record]];
|
||||
if ([[record objectForKey: @"c_deleted"] intValue] > 0)
|
||||
[children addObject: davElementWithContent (@"status", XMLNS_WEBDAV, @"HTTP/1.1 404 Not Found")];
|
||||
else
|
||||
[children addObjectsFromArray: [self _davPropstatsWithProperties: properties
|
||||
andMethodSelectors: selectors
|
||||
fromRecord: record]];
|
||||
|
||||
return davElementWithContent (@"response", XMLNS_WEBDAV, children);
|
||||
}
|
||||
@@ -1513,6 +1490,10 @@ static NSArray *childRecordFields = nil;
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
DAV:sync-collection Report
|
||||
https://datatracker.ietf.org/doc/html/rfc6578#section-3.2
|
||||
*/
|
||||
- (WOResponse *) davSyncCollection: (WOContext *) localContext
|
||||
{
|
||||
WOResponse *r;
|
||||
@@ -1948,8 +1929,8 @@ static NSArray *childRecordFields = nil;
|
||||
if (sqlFilter)
|
||||
{
|
||||
filterString = [NSMutableString stringWithCapacity: 8192];
|
||||
[filterString appendFormat: @"(c_name='%@')",
|
||||
[cNames componentsJoinedByString: @"' OR c_name='"]];
|
||||
[filterString appendFormat: @"(c_name = '%@')",
|
||||
[cNames componentsJoinedByString: @"' OR c_name = '"]];
|
||||
if ([sqlFilter length] > 0)
|
||||
[filterString appendFormat: @" AND (%@)", sqlFilter];
|
||||
qualifier = [EOQualifier qualifierWithQualifierFormat: filterString];
|
||||
@@ -1989,10 +1970,9 @@ static NSArray *childRecordFields = nil;
|
||||
components = [NSMutableArray arrayWithCapacity: max];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
currentName = [cNames objectAtIndex: count];
|
||||
currentName = [[cNames objectAtIndex: count] asSafeSQLString];
|
||||
queryNameLength = idQueryOverhead + [currentName length];
|
||||
if ((currentSize + queryNameLength)
|
||||
> maxQuerySize)
|
||||
if ((currentSize + queryNameLength) > maxQuerySize)
|
||||
{
|
||||
records = [self _fetchComponentsWithNames: currentNames fields: fields];
|
||||
[components addObjectsFromArray: records];
|
||||
@@ -2003,8 +1983,11 @@ static NSArray *childRecordFields = nil;
|
||||
currentSize += queryNameLength;
|
||||
}
|
||||
|
||||
records = [self _fetchComponentsWithNames: currentNames fields: fields];
|
||||
[components addObjectsFromArray: records];
|
||||
if ([currentNames count])
|
||||
{
|
||||
records = [self _fetchComponentsWithNames: currentNames fields: fields];
|
||||
[components addObjectsFromArray: records];
|
||||
}
|
||||
|
||||
// NSLog (@"/fetching components matching names");
|
||||
|
||||
@@ -2304,6 +2287,10 @@ static NSArray *childRecordFields = nil;
|
||||
NSZoneFree (NULL, propertiesArray);
|
||||
}
|
||||
|
||||
/**
|
||||
CALDAV:calendar-multiget REPORT
|
||||
https://datatracker.ietf.org/doc/html/rfc4791#section-7.9
|
||||
*/
|
||||
- (WOResponse *) performMultigetInContext: (WOContext *) queryContext
|
||||
inNamespace: (NSString *) namespace
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* SOGoWebAuthenticator.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2007-2014 Inverse inc.
|
||||
* Copyright (C) 2007-2022 Inverse inc.
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -390,6 +390,7 @@
|
||||
{
|
||||
WOCookie *authCookie;
|
||||
NSString *cookieValue, *cookieString, *appName, *sessionKey, *userKey, *securedPassword;
|
||||
BOOL isSecure;
|
||||
|
||||
//
|
||||
// We create a new cookie - thus we create a new session
|
||||
@@ -416,8 +417,14 @@
|
||||
userKey, sessionKey];
|
||||
cookieValue = [NSString stringWithFormat: @"basic %@",
|
||||
[cookieString stringByEncodingBase64]];
|
||||
isSecure = [[[context serverURL] scheme] isEqualToString: @"https"];
|
||||
authCookie = [WOCookie cookieWithName: [self cookieNameInContext: context]
|
||||
value: cookieValue];
|
||||
value: cookieValue
|
||||
path: nil
|
||||
domain: nil
|
||||
expires: nil
|
||||
isSecure: isSecure
|
||||
httpOnly: YES];
|
||||
appName = [[context request] applicationName];
|
||||
[authCookie setPath: [NSString stringWithFormat: @"/%@/", appName]];
|
||||
|
||||
|
||||
@@ -776,7 +776,7 @@
|
||||
if (channel)
|
||||
{
|
||||
lowerFilter = [filter lowercaseString];
|
||||
lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"];
|
||||
lowerFilter = [lowerFilter asSafeSQLLikeString];
|
||||
|
||||
sql = [NSMutableString stringWithFormat: (@"SELECT *"
|
||||
@" FROM %@"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import os, sys, unittest, getopt, traceback, time
|
||||
import preferences
|
||||
import sogotests
|
||||
import unittest
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest._TextTestResult.oldStartTest = unittest._TextTestResult.startTest
|
||||
unittest._TextTestResult.startTest = sogotests.UnitTestTextTestResultNewStartTest
|
||||
unittest._TextTestResult.stopTest = sogotests.UnitTestTextTestResultNewStopTest
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
modules = []
|
||||
|
||||
languages = preferences.SOGoSupportedLanguages
|
||||
|
||||
# We can disable testing all languages
|
||||
testLanguages = False
|
||||
opts, args = getopt.getopt (sys.argv[1:], [], ["enable-languages"])
|
||||
for o, a in opts:
|
||||
if o == "--enable-languages":
|
||||
testLanguages = True
|
||||
|
||||
|
||||
for mod in os.listdir("."):
|
||||
if mod.startswith("test-") and mod.endswith(".py"):
|
||||
modules.append(mod[:-3])
|
||||
__import__(mod[:-3])
|
||||
|
||||
if len(modules) > 0:
|
||||
suite = loader.loadTestsFromNames(modules)
|
||||
print "%d tests in modules: '%s'" % (suite.countTestCases(),
|
||||
"', '".join(modules))
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
|
||||
if testLanguages:
|
||||
prefs = preferences.preferences()
|
||||
# Get the current language
|
||||
userLanguageString = prefs.get ("SOGoLanguage")
|
||||
if userLanguageString:
|
||||
userLanguage = languages.index (userLanguageString)
|
||||
else:
|
||||
userLanguage = languages.index ("English")
|
||||
|
||||
for i in range (0, len (languages)):
|
||||
try:
|
||||
prefs.set ("SOGoLanguage", i)
|
||||
except Exception, inst:
|
||||
print '-' * 60
|
||||
traceback.print_exc ()
|
||||
print '-' * 60
|
||||
|
||||
print "Running test in %s (%d/%d)" % \
|
||||
(languages[i], i + 1, len (languages))
|
||||
runner.verbosity = 2
|
||||
runner.run(suite)
|
||||
# Revert to the original language
|
||||
prefs.set ("SOGoLanguage", userLanguage)
|
||||
else:
|
||||
runner.run(suite)
|
||||
|
||||
else:
|
||||
print "No test available."
|
||||
@@ -1,38 +0,0 @@
|
||||
# setup: 4 user are needed: username, superuser, attendee1, attendee1_delegate
|
||||
# superuser must be a sogo superuser...
|
||||
|
||||
hostname = "localhost"
|
||||
port = "80"
|
||||
username = "myuser"
|
||||
password = "mypass"
|
||||
|
||||
superuser = "super"
|
||||
superuser_password="pass"
|
||||
|
||||
subscriber_username = "otheruser"
|
||||
subscriber_password = "otherpass"
|
||||
|
||||
attendee1 = "user@domain.com"
|
||||
attendee1_username = "user"
|
||||
attendee1_password = "pass"
|
||||
|
||||
attendee1_delegate = "user2@domain.com"
|
||||
attendee1_delegate_username = "sogo2"
|
||||
attendee1_delegate_password = "sogo"
|
||||
|
||||
resource_no_overbook = "res"
|
||||
resource_can_overbook = "res-nolimit"
|
||||
|
||||
white_listed_attendee = '{"sogo1":"John Doe <sogo1@example.com>"}'
|
||||
|
||||
mailserver = "imaphost"
|
||||
|
||||
testput_nbrdays = 30
|
||||
|
||||
sieve_server = "localhost"
|
||||
sieve_port = 2000
|
||||
|
||||
sogo_user = "sogo"
|
||||
sogo_tool_path = "/usr/local/sbin/sogo-tool"
|
||||
|
||||
webCalendarURL = "http://inverse.ca/sogo-integration-tests/CanadaHolidays.ics"
|
||||
@@ -1,92 +0,0 @@
|
||||
import time
|
||||
|
||||
def hours(nbr):
|
||||
return nbr * 3600
|
||||
|
||||
def days(nbr):
|
||||
return nbr * hours(24)
|
||||
|
||||
class ev_generator:
|
||||
ev_templ = """
|
||||
BEGIN:VCALENDAR\r
|
||||
VERSION:2.0\r
|
||||
PRODID:-//Inverse//Event Generator//EN\r
|
||||
CALSCALE:GREGORIAN\r
|
||||
BEGIN:VTIMEZONE\r
|
||||
TZID:America/Montreal\r
|
||||
BEGIN:DAYLIGHT\r
|
||||
TZOFFSETFROM:-0500\r
|
||||
TZOFFSETTO:-0400\r
|
||||
DTSTART:20070311T020000\r
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU\r
|
||||
TZNAME:EDT\r
|
||||
END:DAYLIGHT\r
|
||||
BEGIN:STANDARD\r
|
||||
TZOFFSETFROM:-0400\r
|
||||
TZOFFSETTO:-0500\r
|
||||
DTSTART:20071104T020000\r
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU\r
|
||||
TZNAME:EST\r
|
||||
END:STANDARD\r
|
||||
END:VTIMEZONE\r
|
||||
BEGIN:VEVENT\r
|
||||
SEQUENCE:4\r
|
||||
TRANSP:OPAQUE\r
|
||||
UID:%(uid)s\r
|
||||
SUMMARY:%(summary)s\r
|
||||
DTSTART;TZID=America/Montreal:%(start)s\r
|
||||
DTEND;TZID=America/Montreal:%(end)s\r
|
||||
CREATED:20080711T231608Z\r
|
||||
DTSTAMP:20080711T231640Z\r
|
||||
END:VEVENT\r
|
||||
END:VCALENDAR\r
|
||||
"""
|
||||
def __init__(self, maxDays):
|
||||
self.reset(maxDays)
|
||||
|
||||
def reset(self, maxDays):
|
||||
self.maxDays = maxDays
|
||||
self.currentDay = 0
|
||||
self.currentStart = 0
|
||||
today = time.mktime(time.localtime())
|
||||
self.firstDay = today - days(maxDays + 30)
|
||||
|
||||
def _calendarDate(self, eventTime):
|
||||
timeStruct = time.localtime(eventTime)
|
||||
return time.strftime("%Y%m%dT%H0000", timeStruct)
|
||||
|
||||
def _iterValues(self):
|
||||
event = None
|
||||
|
||||
if (self.currentDay < self.maxDays):
|
||||
eventStart = (self.firstDay
|
||||
+ days(self.currentDay)
|
||||
+ hours(self.currentStart + 8))
|
||||
eventEnd = eventStart + hours(1)
|
||||
|
||||
thatDay = time.localtime(int(eventStart))
|
||||
uid = "Event%d%d" % (eventStart, eventEnd)
|
||||
summary = "%s - event %d" % (time.strftime("%Y-%m-%d", thatDay),
|
||||
self.currentStart)
|
||||
start = self._calendarDate(eventStart)
|
||||
end = self._calendarDate(eventEnd)
|
||||
event = {'uid': uid,
|
||||
'summary': summary,
|
||||
'start': start,
|
||||
'end': end}
|
||||
|
||||
self.currentStart = self.currentStart + 1
|
||||
if (self.currentStart > 7):
|
||||
self.currentStart = 0
|
||||
self.currentDay = self.currentDay + 1
|
||||
|
||||
return event
|
||||
|
||||
def iter(self):
|
||||
hasMore = False
|
||||
entryValues = self._iterValues()
|
||||
if (entryValues is not None):
|
||||
self.event = (self.ev_templ % entryValues).strip()
|
||||
hasMore = True
|
||||
|
||||
return hasMore
|
||||
@@ -1,631 +0,0 @@
|
||||
"""Sieve management client.
|
||||
|
||||
A Protocol for Remotely Managing Sieve Scripts
|
||||
Based on <draft-martin-managesieve-04.txt>
|
||||
"""
|
||||
|
||||
__version__ = "0.4.2"
|
||||
__author__ = """Hartmut Goebel <h.goebel@crazy-compilers.com>
|
||||
Ulrich Eck <ueck@net-labs.de> April 2001
|
||||
"""
|
||||
|
||||
import binascii, re, socket, time, random, sys
|
||||
try:
|
||||
import ssl
|
||||
ssl_wrap_socket = ssl.wrap_socket
|
||||
except ImportError:
|
||||
ssl_wrap_socket = socket.ssl
|
||||
|
||||
__all__ = [ 'MANAGESIEVE', 'SIEVE_PORT', 'OK', 'NO', 'BYE', 'Debug']
|
||||
|
||||
Debug = 0
|
||||
CRLF = '\r\n'
|
||||
SIEVE_PORT = 2000
|
||||
|
||||
OK = 'OK'
|
||||
NO = 'NO'
|
||||
BYE = 'BYE'
|
||||
|
||||
AUTH_PLAIN = "PLAIN"
|
||||
AUTH_LOGIN = "LOGIN"
|
||||
# authentication mechanisms currently supported
|
||||
# in order of preference
|
||||
AUTHMECHS = [AUTH_PLAIN, AUTH_LOGIN]
|
||||
|
||||
# todo: return results or raise exceptions?
|
||||
# todo: on result 'BYE' quit immediatly
|
||||
# todo: raise exception on 'BYE'?
|
||||
|
||||
# Commands
|
||||
commands = {
|
||||
# name valid states
|
||||
'STARTTLS': ('NONAUTH',),
|
||||
'AUTHENTICATE': ('NONAUTH',),
|
||||
'LOGOUT': ('NONAUTH', 'AUTH', 'LOGOUT'),
|
||||
'CAPABILITY': ('NONAUTH', 'AUTH'),
|
||||
'GETSCRIPT': ('AUTH', ),
|
||||
'PUTSCRIPT': ('AUTH', ),
|
||||
'SETACTIVE': ('AUTH', ),
|
||||
'DELETESCRIPT': ('AUTH', ),
|
||||
'LISTSCRIPTS': ('AUTH', ),
|
||||
'HAVESPACE': ('AUTH', ),
|
||||
# bogus command to receive a NO after STARTTLS (see starttls() )
|
||||
'BOGUS': ('NONAUTH', 'AUTH', 'LOGOUT'),
|
||||
}
|
||||
|
||||
### needed
|
||||
Oknobye = re.compile(r'(?P<type>(OK|NO|BYE))'
|
||||
r'( \((?P<code>.*)\))?'
|
||||
r'( (?P<data>.*))?')
|
||||
# draft-martin-managesieve-04.txt defines the size tag of literals to
|
||||
# contain a '+' (plus sign) behind the digits, but timsieved does not
|
||||
# send one. Thus we are less strikt here:
|
||||
Literal = re.compile(r'.*{(?P<size>\d+)\+?}$')
|
||||
re_dquote = re.compile(r'"(([^"\\]|\\.)*)"')
|
||||
re_esc_quote = re.compile(r'\\([\\"])')
|
||||
|
||||
|
||||
class SSLFakeSocket:
|
||||
"""A fake socket object that really wraps a SSLObject.
|
||||
|
||||
It only supports what is needed in managesieve.
|
||||
"""
|
||||
def __init__(self, realsock, sslobj):
|
||||
self.realsock = realsock
|
||||
self.sslobj = sslobj
|
||||
|
||||
def send(self, str):
|
||||
self.sslobj.write(str)
|
||||
return len(str)
|
||||
|
||||
sendall = send
|
||||
|
||||
def close(self):
|
||||
self.realsock.close()
|
||||
|
||||
class SSLFakeFile:
|
||||
"""A fake file like object that really wraps a SSLObject.
|
||||
|
||||
It only supports what is needed in managesieve.
|
||||
"""
|
||||
def __init__(self, sslobj):
|
||||
self.sslobj = sslobj
|
||||
|
||||
def readline(self):
|
||||
str = ""
|
||||
chr = None
|
||||
while chr != "\n":
|
||||
chr = self.sslobj.read(1)
|
||||
str += chr
|
||||
return str
|
||||
|
||||
def read(self, size=0):
|
||||
if size == 0:
|
||||
return ''
|
||||
else:
|
||||
return self.sslobj.read(size)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
def sieve_name(name):
|
||||
# todo: correct quoting
|
||||
return '"%s"' % name
|
||||
|
||||
def sieve_string(string):
|
||||
return '{%d+}%s%s' % ( len(string), CRLF, string )
|
||||
|
||||
|
||||
class MANAGESIEVE:
|
||||
"""Sieve client class.
|
||||
|
||||
Instantiate with: MANAGESIEVE(host [, port])
|
||||
|
||||
host - host's name (default: localhost)
|
||||
port - port number (default: standard Sieve port).
|
||||
|
||||
use_tls - switch to TLS automatically, if server supports
|
||||
keyfile - keyfile to use for TLS (optional)
|
||||
certfile - certfile to use for TLS (optional)
|
||||
|
||||
All Sieve commands are supported by methods of the same
|
||||
name (in lower-case).
|
||||
|
||||
Each command returns a tuple: (type, [data, ...]) where 'type'
|
||||
is usually 'OK' or 'NO', and 'data' is either the text from the
|
||||
tagged response, or untagged results from command.
|
||||
|
||||
All arguments to commands are converted to strings, except for
|
||||
AUTHENTICATE.
|
||||
"""
|
||||
|
||||
"""
|
||||
However, the 'password' argument to the LOGIN command is always
|
||||
quoted. If you want to avoid having an argument string quoted (eg:
|
||||
the 'flags' argument to STORE) then enclose the string in
|
||||
parentheses (eg: "(\Deleted)").
|
||||
|
||||
Errors raise the exception class <instance>.error("<reason>").
|
||||
IMAP4 server errors raise <instance>.abort("<reason>"),
|
||||
which is a sub-class of 'error'. Mailbox status changes
|
||||
from READ-WRITE to READ-ONLY raise the exception class
|
||||
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
|
||||
|
||||
"error" exceptions imply a program error.
|
||||
"abort" exceptions imply the connection should be reset, and
|
||||
the command re-tried.
|
||||
"readonly" exceptions imply the command should be re-tried.
|
||||
|
||||
Note: to use this module, you must read the RFCs pertaining
|
||||
to the IMAP4 protocol, as the semantics of the arguments to
|
||||
each IMAP4 command are left to the invoker, not to mention
|
||||
the results.
|
||||
"""
|
||||
|
||||
class error(Exception): """Logical errors - debug required"""
|
||||
class abort(error): """Service errors - close and retry"""
|
||||
|
||||
def __clear_knowledge(self):
|
||||
"""clear/init any knowledge obtained from the server"""
|
||||
self.capabilities = []
|
||||
self.loginmechs = []
|
||||
self.implementation = ''
|
||||
self.supports_tls = 0
|
||||
|
||||
def __init__(self, host='', port=SIEVE_PORT,
|
||||
use_tls=False, keyfile=None, certfile=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.debug = Debug
|
||||
self.state = 'NONAUTH'
|
||||
|
||||
self.response_text = self.response_code = None
|
||||
self.__clear_knowledge()
|
||||
|
||||
# Open socket to server.
|
||||
self._open(host, port)
|
||||
|
||||
if __debug__:
|
||||
self._cmd_log_len = 10
|
||||
self._cmd_log_idx = 0
|
||||
self._cmd_log = {} # Last `_cmd_log_len' interactions
|
||||
if self.debug >= 1:
|
||||
self._mesg('managesieve version %s' % __version__)
|
||||
|
||||
# Get server welcome message,
|
||||
# request and store CAPABILITY response.
|
||||
typ, data = self._get_response()
|
||||
if typ == 'OK':
|
||||
self._parse_capabilities(data)
|
||||
if use_tls and self.supports_tls:
|
||||
typ, data = self.starttls(keyfile=keyfile, certfile=certfile)
|
||||
if typ == 'OK':
|
||||
self._parse_capabilities(data)
|
||||
|
||||
|
||||
def _parse_capabilities(self, lines):
|
||||
for line in lines:
|
||||
if len(line) == 2:
|
||||
typ, data = line
|
||||
else:
|
||||
assert len(line) == 1, 'Bad Capabilities line: %r' % line
|
||||
typ = line[0]
|
||||
data = None
|
||||
if __debug__:
|
||||
if self.debug >= 3:
|
||||
self._mesg('%s: %r' % (typ, data))
|
||||
if typ == "IMPLEMENTATION":
|
||||
self.implementation = data
|
||||
elif typ == "SASL":
|
||||
self.loginmechs = data.split()
|
||||
elif typ == "SIEVE":
|
||||
self.capabilities = data.split()
|
||||
elif typ == "STARTTLS":
|
||||
self.supports_tls = 1
|
||||
else:
|
||||
# A client implementation MUST ignore any other
|
||||
# capabilities given that it does not understand.
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# Allow UPPERCASE variants of MANAGESIEVE command methods.
|
||||
if commands.has_key(attr):
|
||||
return getattr(self, attr.lower())
|
||||
raise AttributeError("Unknown MANAGESIEVE command: '%s'" % attr)
|
||||
|
||||
|
||||
#### Private methods ###
|
||||
def _open(self, host, port):
|
||||
"""Setup 'self.sock' and 'self.file'."""
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((self.host, self.port))
|
||||
self.file = self.sock.makefile('r')
|
||||
|
||||
def _close(self):
|
||||
self.file.close()
|
||||
self.sock.close()
|
||||
|
||||
def _read(self, size):
|
||||
"""Read 'size' bytes from remote."""
|
||||
data = ""
|
||||
while len(data) < size:
|
||||
data += self.file.read(size - len(data))
|
||||
return data
|
||||
|
||||
def _readline(self):
|
||||
"""Read line from remote."""
|
||||
return self.file.readline()
|
||||
|
||||
def _send(self, data):
|
||||
return self.sock.send(data)
|
||||
|
||||
def _get_line(self):
|
||||
line = self._readline()
|
||||
if not line:
|
||||
raise self.abort('socket error: EOF')
|
||||
# Protocol mandates all lines terminated by CRLF
|
||||
line = line[:-2]
|
||||
if __debug__:
|
||||
if self.debug >= 4:
|
||||
self._mesg('< %s' % line)
|
||||
else:
|
||||
self._log('< %s' % line)
|
||||
return line
|
||||
|
||||
def _simple_command(self, *args):
|
||||
"""Execute a command which does only return status.
|
||||
|
||||
Returns (typ) with
|
||||
typ = response type
|
||||
|
||||
The responce code and text may be found in <instance>.response_code
|
||||
and <instance>.response_text, respectivly.
|
||||
"""
|
||||
return self._command(*args)[0] # only return typ, ignore data
|
||||
|
||||
|
||||
def _command(self, name, arg1=None, arg2=None, *options):
|
||||
"""
|
||||
Returns (typ, data) with
|
||||
typ = response type
|
||||
data = list of lists of strings read (only meaningfull if OK)
|
||||
|
||||
The responce code and text may be found in <instance>.response_code
|
||||
and <instance>.response_text, respectivly.
|
||||
"""
|
||||
if self.state not in commands[name]:
|
||||
raise self.error(
|
||||
'Command %s illegal in state %s' % (name, self.state))
|
||||
# concatinate command and arguments (if any)
|
||||
data = " ".join(filter(None, (name, arg1, arg2)))
|
||||
if __debug__:
|
||||
if self.debug >= 4: self._mesg('> %r' % data)
|
||||
else: self._log('> %s' % data)
|
||||
try:
|
||||
try:
|
||||
self._send('%s%s' % (data, CRLF))
|
||||
for o in options:
|
||||
if __debug__:
|
||||
if self.debug >= 4: self._mesg('> %r' % o)
|
||||
else: self._log('> %r' % data)
|
||||
self._send('%s%s' % (o, CRLF))
|
||||
except (socket.error, OSError), val:
|
||||
raise self.abort('socket error: %s' % val)
|
||||
return self._get_response()
|
||||
except self.abort, val:
|
||||
if __debug__:
|
||||
if self.debug >= 1:
|
||||
self.print_log()
|
||||
raise
|
||||
|
||||
|
||||
def _readstring(self, data):
|
||||
if data[0] == ' ': # space -> error
|
||||
raise self.error('Unexpected space: %r' % data)
|
||||
elif data[0] == '"': # handle double quote:
|
||||
if not self._match(re_dquote, data):
|
||||
raise self.error('Unmatched quote: %r' % data)
|
||||
snippet = self.mo.group(1)
|
||||
return re_esc_quote.sub(r'\1', snippet), data[self.mo.end():]
|
||||
elif self._match(Literal, data):
|
||||
# read a 'literal' string
|
||||
size = int(self.mo.group('size'))
|
||||
if __debug__:
|
||||
if self.debug >= 4:
|
||||
self._mesg('read literal size %s' % size)
|
||||
return self._read(size), self._get_line()
|
||||
else:
|
||||
data = data.split(' ', 1)
|
||||
if len(data) == 1:
|
||||
data.append('')
|
||||
return data
|
||||
|
||||
def _get_response(self):
|
||||
"""
|
||||
Returns (typ, data) with
|
||||
typ = response type
|
||||
data = list of lists of strings read (only meaningfull if OK)
|
||||
|
||||
The responce code and text may be found in <instance>.response_code
|
||||
and <instance>.response_text, respectivly.
|
||||
"""
|
||||
|
||||
"""
|
||||
response-deletescript = response-oknobye
|
||||
response-authenticate = *(string CRLF) (response-oknobye)
|
||||
response-capability = *(string [SP string] CRLF) response-oknobye
|
||||
response-listscripts = *(string [SP "ACTIVE"] CRLF) response-oknobye
|
||||
response-oknobye = ("OK" / "NO" / "BYE") [SP "(" resp-code ")"] [SP string] CRLF
|
||||
string = quoted / literal
|
||||
quoted = <"> *QUOTED-CHAR <">
|
||||
literal = "{" number "+}" CRLF *OCTET
|
||||
;; The number represents the number of octets
|
||||
;; MUST be literal-utf8 except for values
|
||||
|
||||
--> a response either starts with a quote-charakter, a left-bracket or
|
||||
OK, NO, BYE
|
||||
|
||||
"quoted" CRLF
|
||||
"quoted" SP "quoted" CRLF
|
||||
{size} CRLF *OCTETS CRLF
|
||||
{size} CRLF *OCTETS CRLF
|
||||
[A-Z-]+ CRLF
|
||||
|
||||
"""
|
||||
data = [] ; dat = None
|
||||
resp = self._get_line()
|
||||
while 1:
|
||||
if self._match(Oknobye, resp):
|
||||
typ, code, dat = self.mo.group('type','code','data')
|
||||
if __debug__:
|
||||
if self.debug >= 1:
|
||||
self._mesg('%s response: %s %s' % (typ, code, dat))
|
||||
self.response_code = code
|
||||
self.response_text = None
|
||||
if dat:
|
||||
self.response_text = self._readstring(dat)[0]
|
||||
|
||||
# if server quits here, send code instead of empty data
|
||||
if typ == "BYE":
|
||||
return typ, code
|
||||
|
||||
return typ, data
|
||||
## elif 0:
|
||||
## dat2 = None
|
||||
## dat, resp = self._readstring(resp)
|
||||
## if resp.startswith(' '):
|
||||
## dat2, resp = self._readstring(resp[1:])
|
||||
## data.append( (dat, dat2))
|
||||
## resp = self._get_line()
|
||||
else:
|
||||
dat = []
|
||||
while 1:
|
||||
dat1, resp = self._readstring(resp)
|
||||
if __debug__:
|
||||
if self.debug >= 4:
|
||||
self._mesg('read: %r' % (dat1,))
|
||||
if self.debug >= 5:
|
||||
self._mesg('rest: %r' % (resp,))
|
||||
dat.append(dat1)
|
||||
if not resp.startswith(' '):
|
||||
break
|
||||
resp = resp[1:]
|
||||
if len(dat) == 1:
|
||||
dat.append(None)
|
||||
data.append(dat)
|
||||
resp = self._get_line()
|
||||
return self.error('Should not come here')
|
||||
|
||||
|
||||
def _match(self, cre, s):
|
||||
# Run compiled regular expression match method on 's'.
|
||||
# Save result, return success.
|
||||
self.mo = cre.match(s)
|
||||
if __debug__:
|
||||
if self.mo is not None and self.debug >= 5:
|
||||
self._mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
|
||||
return self.mo is not None
|
||||
|
||||
|
||||
if __debug__:
|
||||
|
||||
def _mesg(self, s, secs=None):
|
||||
if secs is None:
|
||||
secs = time.time()
|
||||
tm = time.strftime('%M:%S', time.localtime(secs))
|
||||
sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
|
||||
sys.stderr.flush()
|
||||
|
||||
def _log(self, line):
|
||||
# Keep log of last `_cmd_log_len' interactions for debugging.
|
||||
self._cmd_log[self._cmd_log_idx] = (line, time.time())
|
||||
self._cmd_log_idx += 1
|
||||
if self._cmd_log_idx >= self._cmd_log_len:
|
||||
self._cmd_log_idx = 0
|
||||
|
||||
def print_log(self):
|
||||
self.self._mesg('last %d SIEVE interactions:' % len(self._cmd_log))
|
||||
i, n = self._cmd_log_idx, self._cmd_log_len
|
||||
while n:
|
||||
try:
|
||||
self.self._mesg(*self._cmd_log[i])
|
||||
except:
|
||||
pass
|
||||
i += 1
|
||||
if i >= self._cmd_log_len:
|
||||
i = 0
|
||||
n -= 1
|
||||
|
||||
### Public methods ###
|
||||
def authenticate(self, mechanism, *authobjects):
|
||||
"""Authenticate command - requires response processing."""
|
||||
# command-authenticate = "AUTHENTICATE" SP auth-type [SP string] *(CRLF string)
|
||||
# response-authenticate = *(string CRLF) (response-oknobye)
|
||||
mech = mechanism.upper()
|
||||
if not mech in self.loginmechs:
|
||||
raise self.error("Server doesn't allow %s authentication." % mech)
|
||||
|
||||
if mech == AUTH_LOGIN:
|
||||
authobjects = [ sieve_name(binascii.b2a_base64(ao)[:-1])
|
||||
for ao in authobjects
|
||||
]
|
||||
elif mech == AUTH_PLAIN:
|
||||
if len(authobjects) < 3:
|
||||
# assume authorization identity (authzid) is missing
|
||||
# and these two authobjects are username and password
|
||||
authobjects.insert(0, '')
|
||||
ao = '\0'.join(authobjects)
|
||||
ao = binascii.b2a_base64(ao)[:-1]
|
||||
authobjects = [ sieve_string(ao) ]
|
||||
else:
|
||||
raise self.error("managesieve doesn't support %s authentication." % mech)
|
||||
|
||||
typ, data = self._command('AUTHENTICATE',
|
||||
sieve_name(mech), *authobjects)
|
||||
if typ == 'OK':
|
||||
self.state = 'AUTH'
|
||||
return typ
|
||||
|
||||
|
||||
def login(self, auth, user, password):
|
||||
"""
|
||||
Authenticate to the Sieve server using the best mechanism available.
|
||||
"""
|
||||
for authmech in AUTHMECHS:
|
||||
if authmech in self.loginmechs:
|
||||
authobjs = [auth, user, password]
|
||||
if authmech == AUTH_LOGIN:
|
||||
authobjs = [user, password]
|
||||
return self.authenticate(authmech, *authobjs)
|
||||
else:
|
||||
raise self.abort('No matching authentication mechanism found.')
|
||||
|
||||
def logout(self):
|
||||
"""Terminate connection to server."""
|
||||
# command-logout = "LOGOUT" CRLF
|
||||
# response-logout = response-oknobye
|
||||
typ = self._simple_command('LOGOUT')
|
||||
self.state = 'LOGOUT'
|
||||
self._close()
|
||||
return typ
|
||||
|
||||
|
||||
def listscripts(self):
|
||||
"""Get a list of scripts on the server.
|
||||
|
||||
(typ, [data]) = <instance>.listscripts()
|
||||
|
||||
if 'typ' is 'OK', 'data' is list of (scriptname, active) tuples.
|
||||
"""
|
||||
# command-listscripts = "LISTSCRIPTS" CRLF
|
||||
# response-listscripts = *(sieve-name [SP "ACTIVE"] CRLF) response-oknobye
|
||||
typ, data = self._command('LISTSCRIPTS')
|
||||
if typ != 'OK': return typ, data
|
||||
scripts = []
|
||||
for dat in data:
|
||||
if __debug__:
|
||||
if not len(dat) in (1, 2):
|
||||
self.error("Unexpected result from LISTSCRIPTS: %r" (dat,))
|
||||
scripts.append( (dat[0], dat[1] is not None ))
|
||||
return typ, scripts
|
||||
|
||||
|
||||
def getscript(self, scriptname):
|
||||
"""Get a script from the server.
|
||||
|
||||
(typ, scriptdata) = <instance>.getscript(scriptname)
|
||||
|
||||
'scriptdata' is the script data.
|
||||
"""
|
||||
# command-getscript = "GETSCRIPT" SP sieve-name CRLF
|
||||
# response-getscript = [string CRLF] response-oknobye
|
||||
|
||||
typ, data = self._command('GETSCRIPT', sieve_name(scriptname))
|
||||
if typ != 'OK': return typ, data
|
||||
if len(data) != 1:
|
||||
self.error('GETSCRIPT returned more than one string/script')
|
||||
# todo: decode data?
|
||||
return typ, data[0][0]
|
||||
|
||||
|
||||
def putscript(self, scriptname, scriptdata):
|
||||
"""Put a script onto the server."""
|
||||
# command-putscript = "PUTSCRIPT" SP sieve-name SP string CRLF
|
||||
# response-putscript = response-oknobye
|
||||
return self._simple_command('PUTSCRIPT',
|
||||
sieve_name(scriptname),
|
||||
sieve_string(scriptdata)
|
||||
)
|
||||
|
||||
def deletescript(self, scriptname):
|
||||
"""Delete a scripts at the server."""
|
||||
# command-deletescript = "DELETESCRIPT" SP sieve-name CRLF
|
||||
# response-deletescript = response-oknobye
|
||||
return self._simple_command('DELETESCRIPT', sieve_name(scriptname))
|
||||
|
||||
|
||||
def setactive(self, scriptname):
|
||||
"""Mark a script as the 'active' one."""
|
||||
# command-setactive = "SETACTIVE" SP sieve-name CRLF
|
||||
# response-setactive = response-oknobye
|
||||
return self._simple_command('SETACTIVE', sieve_name(scriptname))
|
||||
|
||||
|
||||
def havespace(self, scriptname, size):
|
||||
# command-havespace = "HAVESPACE" SP sieve-name SP number CRLF
|
||||
# response-havespace = response-oknobye
|
||||
return self._simple_command('HAVESPACE',
|
||||
sieve_name(scriptname),
|
||||
str(size))
|
||||
|
||||
|
||||
def capability(self):
|
||||
"""
|
||||
Isse a CAPABILITY command and return the result.
|
||||
|
||||
As a side-effect, on succes these attributes are (re)set:
|
||||
self.implementation
|
||||
self.loginmechs
|
||||
self.capabilities
|
||||
self.supports_tls
|
||||
"""
|
||||
# command-capability = "CAPABILITY" CRLF
|
||||
# response-capability = *(string [SP string] CRLF) response-oknobye
|
||||
typ, data = self._command('CAPABILITY')
|
||||
if typ == 'OK':
|
||||
self._parse_capabilities(data)
|
||||
return typ, data
|
||||
|
||||
|
||||
def starttls(self, keyfile=None, certfile=None):
|
||||
"""Puts the connection to the SIEVE server into TLS mode.
|
||||
|
||||
If the server supports TLS, this will encrypt the rest of the SIEVE
|
||||
session. If you provide the keyfile and certfile parameters,
|
||||
the identity of the SIEVE server and client can be checked. This,
|
||||
however, depends on whether the socket module really checks the
|
||||
certificates.
|
||||
"""
|
||||
# command-starttls = "STARTTLS" CRLF
|
||||
# response-starttls = response-oknobye
|
||||
typ, data = self._command('STARTTLS')
|
||||
if typ == 'OK':
|
||||
sslobj = ssl_wrap_socket(self.sock, keyfile, certfile)
|
||||
self.sock = SSLFakeSocket(self.sock, sslobj)
|
||||
self.file = SSLFakeFile(sslobj)
|
||||
# MUST discard knowledge obtained from the server
|
||||
self.__clear_knowledge()
|
||||
# Some servers send capabilities after TLS handshake, some
|
||||
# do not. We send a bogus command, and expect a NO. If you
|
||||
# get something else instead, read the extra NO to clear
|
||||
# the buffer.
|
||||
typ, data = self._command('BOGUS')
|
||||
if typ != 'NO':
|
||||
typ, data = self._get_response()
|
||||
# server may not advertise capabilities, thus we need to ask
|
||||
self.capability()
|
||||
if self.debug >= 3: self._mesg('started Transport Layer Security (TLS)')
|
||||
return typ, data
|
||||
@@ -1,139 +0,0 @@
|
||||
from config import hostname, port, username, password
|
||||
import webdavlib
|
||||
import urllib
|
||||
import base64
|
||||
import simplejson
|
||||
|
||||
import sogoLogin
|
||||
|
||||
|
||||
|
||||
# must be kept in sync with SoObjects/SOGo/SOGoDefaults.plist
|
||||
# this should probably be fetched magically...
|
||||
SOGoSupportedLanguages = [ "Arabic", "Basque", "Catalan", "ChineseChina", "ChineseTaiwan", "Croatian", "Czech", "Dutch", "Danish", "Welsh", "English", "Finnish",
|
||||
"SpanishSpain", "SpanishArgentina", "French", "German", "Hebrew",
|
||||
"Icelandic", "Italian", "Latvian", "Lithuanian", "Macedonian", "Hungarian", "Portuguese", "BrazilianPortuguese",
|
||||
"NorwegianBokmal", "NorwegianNynorsk", "Polish", "Russian", "Serbian", "Slovak",
|
||||
"Slovenian", "Swedish", "TurkishTurkey", "Ukrainian" ];
|
||||
daysBetweenResponseList=[1,2,3,5,7,14,21,30]
|
||||
|
||||
class HTTPPreferencesPOST (webdavlib.HTTPPOST):
|
||||
cookie = None
|
||||
|
||||
def prepare_headers (self):
|
||||
headers = webdavlib.HTTPPOST.prepare_headers(self)
|
||||
if self.cookie:
|
||||
headers["Cookie"] = self.cookie
|
||||
return headers
|
||||
|
||||
class HTTPPreferencesGET (webdavlib.HTTPGET):
|
||||
cookie = None
|
||||
|
||||
def prepare_headers (self):
|
||||
headers = webdavlib.HTTPGET.prepare_headers(self)
|
||||
if self.cookie:
|
||||
headers["Cookie"] = self.cookie
|
||||
return headers
|
||||
|
||||
class preferences:
|
||||
login = username
|
||||
passw = password
|
||||
|
||||
def __init__(self, otherLogin = None, otherPassword = None):
|
||||
if otherLogin and otherPassword:
|
||||
self.login = otherLogin
|
||||
self.passw = otherPassword
|
||||
|
||||
self.client = webdavlib.WebDAVClient(hostname, port)
|
||||
|
||||
authCookie = sogoLogin.getAuthCookie(hostname, port, self.login, self.passw)
|
||||
self.cookie = authCookie
|
||||
|
||||
# map between preferences/jsonDefaults and the webUI names
|
||||
# should probably be unified...
|
||||
self.preferencesMap = {
|
||||
"SOGoLanguage": "language",
|
||||
"SOGoTimeZone": "timezone",
|
||||
"SOGoSieveFilters": "sieveFilters",
|
||||
|
||||
# Vacation stuff
|
||||
"Vacation": "enableVacation", # to disable, don't specify it
|
||||
"autoReplyText": "autoReplyText", # string
|
||||
"autoReplyEmailAddresses": "autoReplyEmailAddresses", # LIST
|
||||
"daysBetweenResponse": "daysBetweenResponsesList",
|
||||
"ignoreLists": "ignoreLists", #bool
|
||||
|
||||
# forward stuff
|
||||
"Forward": "enableForward", # to disable, don't specify it
|
||||
"forwardAddress": "forwardAddress",
|
||||
"keepCopy": "forwardKeepCopy",
|
||||
|
||||
# Calendar stuff
|
||||
"enablePreventInvitations": "preventInvitations",
|
||||
"PreventInvitations": "PreventInvitations",
|
||||
"whiteList": "whiteList",
|
||||
}
|
||||
|
||||
def set(self, preference, value=None):
|
||||
# if preference is a dict, set all prefs found in the dict
|
||||
content=""
|
||||
if isinstance(preference, dict):
|
||||
for k,v in preference.items():
|
||||
content+="%s=%s&" % (self.preferencesMap[k], urllib.quote(v))
|
||||
else:
|
||||
# assume it is a str
|
||||
formKey = self.preferencesMap[preference]
|
||||
content = "%s=%s&hasChanged=1" % (formKey, urllib.quote(value))
|
||||
|
||||
|
||||
url = "/SOGo/so/%s/preferences" % self.login
|
||||
|
||||
post = HTTPPreferencesPOST (url, content)
|
||||
post.content_type = "application/x-www-form-urlencoded"
|
||||
post.cookie = self.cookie
|
||||
|
||||
self.client.execute (post)
|
||||
|
||||
# Raise an exception if the pref wasn't properly set
|
||||
if post.response["status"] != 200:
|
||||
raise Exception ("failure setting prefs, (code = %d)" \
|
||||
% post.response["status"])
|
||||
|
||||
def get(self, preference=None):
|
||||
url = "/SOGo/so/%s/preferences/jsonDefaults" % self.login
|
||||
get = HTTPPreferencesGET (url)
|
||||
get.cookie = self.cookie
|
||||
self.client.execute (get)
|
||||
content = simplejson.loads(get.response['body'])
|
||||
result = None
|
||||
try:
|
||||
if preference:
|
||||
result = content[preference]
|
||||
else:
|
||||
result = content
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
def get_settings(self, preference=None):
|
||||
url = "/SOGo/so/%s/preferences/jsonSettings" % self.login
|
||||
get = HTTPPreferencesGET (url)
|
||||
get.cookie = self.cookie
|
||||
self.client.execute (get)
|
||||
content = simplejson.loads(get.response['body'])
|
||||
result = None
|
||||
try:
|
||||
if preference:
|
||||
result = content[preference]
|
||||
else:
|
||||
result = content
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
|
||||
# Simple main to test this class
|
||||
if __name__ == "__main__":
|
||||
p = preferences ()
|
||||
print p.get ("SOGoLanguage")
|
||||
p.set ("SOGoLanguage", SOGoSupportedLanguages.index("French"))
|
||||
print p.get ("SOGoLanguage")
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password
|
||||
|
||||
import webdavlib
|
||||
|
||||
import sys
|
||||
import getopt
|
||||
import xml.dom.minidom
|
||||
|
||||
def parseArguments():
|
||||
arguments = {}
|
||||
|
||||
depth = "0"
|
||||
quiet = False
|
||||
(opts, args) = getopt.getopt(sys.argv[1:], "d:q", ["depth=", "quiet"])
|
||||
|
||||
for pair in opts:
|
||||
if (pair[0] == "-d" or pair[0] == "--depth"):
|
||||
depth = pair[1]
|
||||
elif (pair[0] == "-q" or pair[0] == "--quiet"):
|
||||
quiet = True
|
||||
|
||||
# print "depth: " + depth
|
||||
|
||||
nargs = len(args)
|
||||
if (nargs > 0):
|
||||
resource = args[0]
|
||||
if (nargs > 1):
|
||||
properties = args[1:]
|
||||
else:
|
||||
properties = [ "allprop" ]
|
||||
else:
|
||||
print "resource required"
|
||||
sys.exit(-1)
|
||||
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource, properties, depth)
|
||||
client.execute(propfind)
|
||||
|
||||
sys.stderr.write("response:\n\n")
|
||||
print propfind.response["body"]
|
||||
|
||||
if propfind.response.has_key("document"):
|
||||
sys.stderr.write("document tree:\n")
|
||||
elem = propfind.response["document"]
|
||||
dom = xml.dom.minidom.parseString(xml.etree.ElementTree.tostring(elem))
|
||||
print dom.toprettyxml()
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password
|
||||
import sys, getopt
|
||||
import webdavlib
|
||||
import urllib
|
||||
import urllib2
|
||||
import base64
|
||||
import simplejson
|
||||
import cookielib
|
||||
|
||||
def getAuthCookie(hostname, port, username, password) :
|
||||
cjar = cookielib.CookieJar();
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cjar))
|
||||
urllib2.install_opener(opener)
|
||||
|
||||
creds = urllib.urlencode([("userName",username), ("password", password)])
|
||||
req = urllib2.Request("http://%s:%s/SOGo/connect" % (hostname, port), creds)
|
||||
|
||||
fd = urllib2.urlopen(req)
|
||||
|
||||
for cookie in cjar :
|
||||
if cookie.name == "0xHIGHFLYxSOGo":
|
||||
authinfo = cookie.value
|
||||
break
|
||||
|
||||
return "0xHIGHFLYxSOGo="+authinfo
|
||||
|
||||
def usage() :
|
||||
msg ="""Usage:
|
||||
%s [-h] [-H | --host=hostname] [-p|--passwd=password] \
|
||||
[-P|--port=port] [-u|--user=username]\n""" % sys.argv[0]
|
||||
|
||||
sys.stderr.write(msg);
|
||||
|
||||
if __name__ == "__main__" :
|
||||
try:
|
||||
opts, args = getopt.getopt (sys.argv[1:], "hH:p:P:u:", \
|
||||
("host=", "passwd=", "port=", "user="));
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
exit(1)
|
||||
for o, v in opts :
|
||||
if o == "-h" :
|
||||
usage()
|
||||
exit(1)
|
||||
elif o == "-H" or o == "--host" :
|
||||
hostname = v
|
||||
elif o == "-p" or o == "--passwd" :
|
||||
password = v
|
||||
elif o == "-P" or o == "--port" :
|
||||
port = v
|
||||
elif o == "-u" or o == "--user" :
|
||||
username = v
|
||||
|
||||
print getAuthCookie(hostname, port, username, password)
|
||||
@@ -1,23 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
import time
|
||||
|
||||
def UnitTestTextTestResultNewStartTest(self, test):
|
||||
self.xstartTime = time.time()
|
||||
self.oldStartTest(test)
|
||||
|
||||
def UnitTestTextTestResultNewStopTest(self, test):
|
||||
unittest.TestResult.stopTest(self, test)
|
||||
endTime = time.time()
|
||||
delta = endTime - self.xstartTime
|
||||
print " %f ms" % delta
|
||||
|
||||
def runTests():
|
||||
unittest._TextTestResult.oldStartTest = unittest._TextTestResult.startTest
|
||||
unittest._TextTestResult.startTest = UnitTestTextTestResultNewStartTest
|
||||
unittest._TextTestResult.stopTest = UnitTestTextTestResultNewStopTest
|
||||
|
||||
argv = []
|
||||
argv.extend(sys.argv)
|
||||
argv.append("-v")
|
||||
unittest.main(argv=argv)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password, mailserver, subscriber_username, attendee1, attendee1_delegate
|
||||
|
||||
import sogotests
|
||||
import unittest
|
||||
import time
|
||||
|
||||
class CalDAVITIPDelegationTest(unittest.TestCase):
|
||||
def testConfigPY(self):
|
||||
""" config.py validation """
|
||||
try:
|
||||
test = hostname
|
||||
except:
|
||||
self.fail("'hostname' is not defined")
|
||||
|
||||
try:
|
||||
test = username
|
||||
except:
|
||||
self.fail("'username' is not defined")
|
||||
|
||||
try:
|
||||
test = subscriber_username
|
||||
except:
|
||||
self.fail("'subscriber_username' is not defined")
|
||||
|
||||
try:
|
||||
test = attendee1
|
||||
except:
|
||||
self.fail("'attendee1' is not defined")
|
||||
|
||||
try:
|
||||
test = attendee1_delegate
|
||||
except:
|
||||
self.fail("'attendee1_delegate' is not defined")
|
||||
|
||||
self.assertEquals(subscriber_username, attendee1,
|
||||
"'subscriber_username' and 'attendee1'"
|
||||
+ " must be the same user")
|
||||
|
||||
try:
|
||||
test = mailserver
|
||||
except:
|
||||
self.fail("'mailserver' is not defined")
|
||||
|
||||
userHash = {}
|
||||
userList = [ username, subscriber_username, attendee1_delegate ]
|
||||
for user in userList:
|
||||
self.assertFalse(userHash.has_key(user),
|
||||
"username, attendee1, attendee1_delegate must"
|
||||
+ " all be different users ('%s')"
|
||||
% user)
|
||||
userHash[user] = True
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sogotests
|
||||
import unittest
|
||||
import webdavlib
|
||||
|
||||
from config import *
|
||||
|
||||
class HTTPContactCategoriesTest(unittest.TestCase):
|
||||
def _setCategories(self, user, categories = None):
|
||||
resource = '/SOGo/dav/%s/Contacts/' % user
|
||||
if categories is None:
|
||||
categories = []
|
||||
elements = [ { "{urn:inverse:params:xml:ns:inverse-dav}category": x }
|
||||
for x in categories ]
|
||||
props = { "{urn:inverse:params:xml:ns:inverse-dav}contacts-categories": elements }
|
||||
proppatch = webdavlib.WebDAVPROPPATCH(resource, props)
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
client.execute(proppatch)
|
||||
self.assertEquals(proppatch.response["status"], 207,
|
||||
"failure (%s) setting '%s' categories on %s's contacts"
|
||||
% (proppatch.response["status"],
|
||||
"', '".join(categories), user))
|
||||
|
||||
def _getCategories(self, user):
|
||||
resource = '/SOGo/dav/%s/Contacts/' % user
|
||||
props = [ "{urn:inverse:params:xml:ns:inverse-dav}contacts-categories" ]
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource, props, "0")
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207,
|
||||
"failure (%s) getting categories on %s's contacts"
|
||||
% (propfind.response["status"], user))
|
||||
|
||||
categories = []
|
||||
prop_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{urn:inverse:params:xml:ns:inverse-dav}contacts-categories")
|
||||
for prop_node in prop_nodes:
|
||||
cat_nodes = prop_node.findall("{urn:inverse:params:xml:ns:inverse-dav}category")
|
||||
if cat_nodes is not None:
|
||||
for cat_node in cat_nodes:
|
||||
categories.append(cat_node.text)
|
||||
|
||||
return categories
|
||||
|
||||
|
||||
def test(self):
|
||||
self._setCategories(username, [])
|
||||
cats = self._getCategories(username)
|
||||
self.assertTrue(cats is not None and len(cats) == 0)
|
||||
|
||||
self._setCategories(username, [ "Coucou" ])
|
||||
cats = self._getCategories(username)
|
||||
self.assertTrue(cats is not None and len(cats) == 1)
|
||||
self.assertEquals(cats[0], "Coucou")
|
||||
|
||||
self._setCategories(username, [ "Toto", "Cuicui" ])
|
||||
cats = self._getCategories(username)
|
||||
self.assertTrue(cats is not None and len(cats) == 2)
|
||||
self.assertEquals(cats[0], "Toto")
|
||||
self.assertEquals(cats[1], "Cuicui")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sogotests
|
||||
import unittest
|
||||
import webdavlib
|
||||
|
||||
from config import *
|
||||
|
||||
class HTTPDefaultClassificationTest(unittest.TestCase):
|
||||
def _setClassification(self, user, component, classification = ""):
|
||||
resource = '/SOGo/dav/%s/Calendar/' % user
|
||||
props = { "{urn:inverse:params:xml:ns:inverse-dav}%s-default-classification" % component: classification }
|
||||
proppatch = webdavlib.WebDAVPROPPATCH(resource, props)
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
client.execute(proppatch)
|
||||
|
||||
return (proppatch.response["status"] == 207);
|
||||
|
||||
def _getClassification(self, user, component):
|
||||
resource = '/SOGo/dav/%s/Calendar/' % user
|
||||
property_name = "{urn:inverse:params:xml:ns:inverse-dav}%s-default-classification" % component
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource, [ property_name ], "0")
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
client.execute(propfind)
|
||||
classification = None
|
||||
propstat_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat")
|
||||
for propstat_node in propstat_nodes:
|
||||
status_nodes = propstat_node.findall("{DAV:}status")
|
||||
if status_nodes[0].text.lower() == "http/1.1 200 ok":
|
||||
property_nodes = propstat_node.findall("{DAV:}prop/%s" % property_name)
|
||||
if len(property_nodes) > 0:
|
||||
classification = property_nodes[0].text
|
||||
|
||||
return classification
|
||||
|
||||
def test(self):
|
||||
self.assertFalse(self._setClassification(username, "123456", "PUBLIC"),
|
||||
"expected failure when setting a classification with an invalid property")
|
||||
self.assertFalse(self._setClassification(username, "events", ""),
|
||||
"expected failure when setting an empty classification")
|
||||
self.assertFalse(self._setClassification(username, "events", "pouet"),
|
||||
"expected failure when setting an invalid classification")
|
||||
for component in [ "events", "tasks" ]:
|
||||
for classification in [ "PUBLIC", "PRIVATE", "CONFIDENTIAL" ]:
|
||||
self.assertTrue(self._setClassification(username, component, classification),
|
||||
"error when setting classification to '%s'" % classification)
|
||||
fetched_class = self._getClassification(username, component)
|
||||
self.assertTrue(classification == fetched_class,
|
||||
"set and fetched classifications do not match (%s != %s)" % (classification, fetched_class))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,208 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# FIXME: we should avoid using superuser if possible
|
||||
|
||||
from config import hostname, port, username, password, subscriber_username, \
|
||||
superuser, superuser_password
|
||||
|
||||
import unittest
|
||||
import sogotests
|
||||
import utilities
|
||||
import webdavlib
|
||||
|
||||
class iCalTest(unittest.TestCase):
|
||||
def testPrincipalCollectionSet(self):
|
||||
"""principal-collection-set: 'DAV' header must be returned with iCal 4"""
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
resource = '/SOGo/dav/%s/' % username
|
||||
|
||||
# NOT iCal4
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}principal-collection-set"],
|
||||
0)
|
||||
client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207)
|
||||
headers = propfind.response["headers"]
|
||||
self.assertFalse(headers.has_key("dav"),
|
||||
"DAV header must not be returned when user-agent is NOT iCal 4")
|
||||
|
||||
# iCal4
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}principal-collection-set"],
|
||||
0)
|
||||
client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)"
|
||||
client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207)
|
||||
headers = propfind.response["headers"]
|
||||
self.assertTrue(headers.has_key("dav"),
|
||||
"DAV header must be returned when user-agent is iCal 4")
|
||||
|
||||
expectedDAVClasses = ["1", "2", "access-control", "calendar-access",
|
||||
"calendar-schedule", "calendar-auto-schedule",
|
||||
"calendar-proxy"]
|
||||
davClasses = [x.strip() for x in headers["dav"].split(",")]
|
||||
for davClass in expectedDAVClasses:
|
||||
self.assertTrue(davClass in davClasses,
|
||||
"DAV class '%s' not found" % davClass)
|
||||
|
||||
def _setMemberSet(self, owner, members, perm):
|
||||
resource = '/SOGo/dav/%s/calendar-proxy-%s/' % (owner, perm)
|
||||
membersHref = [ { "{DAV:}href": '/SOGo/dav/%s/' % x }
|
||||
for x in members ]
|
||||
props = { "{DAV:}group-member-set": membersHref }
|
||||
proppatch = webdavlib.WebDAVPROPPATCH(resource, props)
|
||||
client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password)
|
||||
client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)"
|
||||
client.execute(proppatch)
|
||||
self.assertEquals(proppatch.response["status"], 207,
|
||||
"failure (%s) setting '%s' permission for '%s' on %s's calendars"
|
||||
% (proppatch.response["status"], perm,
|
||||
"', '".join(members), owner))
|
||||
|
||||
def _getMembership(self, user):
|
||||
resource = '/SOGo/dav/%s/' % user
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}group-membership"], 0)
|
||||
client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password)
|
||||
client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)"
|
||||
client.execute(propfind)
|
||||
|
||||
hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}group-membership/{DAV:}href")
|
||||
members = [x.text for x in hrefs]
|
||||
|
||||
return members
|
||||
|
||||
def _getProxyFor(self, user, perm):
|
||||
resource = '/SOGo/dav/%s/' % user
|
||||
prop = "{http://calendarserver.org/ns/}calendar-proxy-%s-for" % perm
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource, [prop], 0)
|
||||
client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password)
|
||||
client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)"
|
||||
client.execute(propfind)
|
||||
|
||||
hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{http://calendarserver.org/ns/}calendar-proxy-%s-for/{DAV:}href"
|
||||
% perm)
|
||||
members = [x.text[len("/SOGo/dav/"):-1] for x in hrefs]
|
||||
|
||||
return members
|
||||
|
||||
def testCalendarProxy(self):
|
||||
"""calendar-proxy as used from iCal"""
|
||||
self._setMemberSet(username, [], "read")
|
||||
self._setMemberSet(username, [], "write")
|
||||
self._setMemberSet(subscriber_username, [], "read")
|
||||
self._setMemberSet(subscriber_username, [], "write")
|
||||
self.assertEquals([], self._getMembership(username),
|
||||
"'%s' must have no membership"
|
||||
% username)
|
||||
self.assertEquals([], self._getMembership(subscriber_username),
|
||||
"'%s' must have no membership"
|
||||
% subscriber_username)
|
||||
self.assertEquals([], self._getProxyFor(username, "read"),
|
||||
"'%s' must not be a proxy for anyone" % username)
|
||||
self.assertEquals([], self._getProxyFor(username, "write"),
|
||||
"'%s' must not be a proxy for anyone" % username)
|
||||
self.assertEquals([], self._getProxyFor(subscriber_username, "read"),
|
||||
"'%s' must not be a proxy for anyone" % subscriber_username)
|
||||
self.assertEquals([], self._getProxyFor(subscriber_username, "write"),
|
||||
"'%s' must not be a proxy for anyone" % subscriber_username)
|
||||
|
||||
for perm in ("read", "write"):
|
||||
for users in ((username, subscriber_username),
|
||||
(subscriber_username, username)):
|
||||
self._setMemberSet(users[0], [users[1]], perm)
|
||||
membership = self._getMembership(users[1])
|
||||
self.assertEquals(['/SOGo/dav/%s/calendar-proxy-%s/'
|
||||
% (users[0], perm)],
|
||||
membership,
|
||||
"'%s' must have %s access to %s's calendars"
|
||||
% (users[1], perm, users[0]))
|
||||
proxyFor = self._getProxyFor(users[1], perm)
|
||||
self.assertEquals([users[0]], proxyFor,
|
||||
"'%s' expected to be %s proxy for %s: %s"
|
||||
% (users[1], perm, users[0], proxyFor))
|
||||
|
||||
def _testMapping(self, client, perm, resource, rights):
|
||||
dav_utility = utilities.TestCalendarACLUtility(self, client, resource)
|
||||
dav_utility.setupRights(subscriber_username, rights)
|
||||
|
||||
membership = self._getMembership(subscriber_username)
|
||||
self.assertEquals(['/SOGo/dav/%s/calendar-proxy-%s/'
|
||||
% (username, perm)],
|
||||
membership,
|
||||
"'%s' must have %s access to %s's calendars:\n%s"
|
||||
% (subscriber_username, perm, username, membership))
|
||||
proxyFor = self._getProxyFor(subscriber_username, perm)
|
||||
self.assertEquals([username], proxyFor,
|
||||
"'%s' expected to be %s proxy for %s: %s"
|
||||
% (subscriber_username, perm, username, proxyFor))
|
||||
|
||||
def testCalendarProxy2(self):
|
||||
"""calendar-proxy as used from SOGo"""
|
||||
client = webdavlib.WebDAVClient(hostname, port, superuser, superuser_password)
|
||||
client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)"
|
||||
personal_resource = "/SOGo/dav/%s/Calendar/personal/" % username
|
||||
dav_utility = utilities.TestCalendarACLUtility(self,
|
||||
client,
|
||||
personal_resource)
|
||||
dav_utility.setupRights(subscriber_username, {})
|
||||
dav_utility.subscribe([subscriber_username])
|
||||
|
||||
other_resource = ("/SOGo/dav/%s/Calendar/test-calendar-proxy2/"
|
||||
% username)
|
||||
delete = webdavlib.WebDAVDELETE(other_resource)
|
||||
client.execute(delete)
|
||||
mkcol = webdavlib.WebDAVMKCOL(other_resource)
|
||||
client.execute(mkcol)
|
||||
dav_utility = utilities.TestCalendarACLUtility(self,
|
||||
client,
|
||||
other_resource)
|
||||
dav_utility.setupRights(subscriber_username, {})
|
||||
dav_utility.subscribe([subscriber_username])
|
||||
|
||||
## we test the rights mapping
|
||||
# write: write on 'personal', none on 'test-calendar-proxy2'
|
||||
self._testMapping(client, "write", personal_resource,
|
||||
{ "c": True, "d": False, "pu": "v" })
|
||||
self._testMapping(client, "write", personal_resource,
|
||||
{ "c": False, "d": True, "pu": "v" })
|
||||
self._testMapping(client, "write", personal_resource,
|
||||
{ "c": False, "d": False, "pu": "m" })
|
||||
self._testMapping(client, "write", personal_resource,
|
||||
{ "c": False, "d": False, "pu": "r" })
|
||||
|
||||
# read: read on 'personal', none on 'test-calendar-proxy2'
|
||||
self._testMapping(client, "read", personal_resource,
|
||||
{ "c": False, "d": False, "pu": "d" })
|
||||
self._testMapping(client, "read", personal_resource,
|
||||
{ "c": False, "d": False, "pu": "v" })
|
||||
|
||||
# write: read on 'personal', write on 'test-calendar-proxy2'
|
||||
self._testMapping(client, "write", other_resource,
|
||||
{ "c": False, "d": False, "pu": "r" })
|
||||
|
||||
## we test the unsubscription
|
||||
# unsubscribed from personal, subscribed to 'test-calendar-proxy2'
|
||||
dav_utility = utilities.TestCalendarACLUtility(self, client,
|
||||
personal_resource)
|
||||
dav_utility.unsubscribe([subscriber_username])
|
||||
membership = self._getMembership(subscriber_username)
|
||||
self.assertEquals(['/SOGo/dav/%s/calendar-proxy-write/' % username],
|
||||
membership,
|
||||
"'%s' must have write access to %s's calendars"
|
||||
% (subscriber_username, username))
|
||||
# unsubscribed from personal, unsubscribed from 'test-calendar-proxy2'
|
||||
dav_utility = utilities.TestCalendarACLUtility(self, client,
|
||||
other_resource)
|
||||
dav_utility.unsubscribe([subscriber_username])
|
||||
membership = self._getMembership(subscriber_username)
|
||||
self.assertEquals([],
|
||||
membership,
|
||||
"'%s' must have no access to %s's calendars"
|
||||
% (subscriber_username, username))
|
||||
|
||||
delete = webdavlib.WebDAVDELETE(other_resource)
|
||||
client.execute(delete)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,677 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password, mailserver, subscriber_username, subscriber_password
|
||||
|
||||
import sys
|
||||
import sogotests
|
||||
import unittest
|
||||
import webdavlib
|
||||
import time
|
||||
|
||||
# TODO
|
||||
# add test with multiple sort criterias
|
||||
|
||||
def fetchUserEmail(login):
|
||||
client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
resource = '/SOGo/dav/%s/' % login
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"],
|
||||
0)
|
||||
client.execute(propfind)
|
||||
nodes = propfind.xpath_evaluate('{DAV:}response/{DAV:}propstat/{DAV:}prop/C:calendar-user-address-set/{DAV:}href',
|
||||
None)
|
||||
|
||||
return nodes[0].childNodes[0].nodeValue
|
||||
|
||||
message1 = """Return-Path: <cyril@cyril.dev>
|
||||
Received: from cyril.dev (localhost [127.0.0.1])
|
||||
by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA;
|
||||
Tue, 17 Dec 2009 07:42:16 -0400
|
||||
Received: from aloha.dev (localhost [127.0.0.1])
|
||||
by aloha.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA;
|
||||
Tue, 29 Sep 2009 07:42:16 -0400
|
||||
Message-ID: <4AC1F29sept6.5060801@cyril.dev>
|
||||
Date: Mon, 28 Sep 2009 07:42:14 -0400
|
||||
From: Cyril <message1from@cyril.dev>
|
||||
User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605)
|
||||
References: <4AC3BF1B.3010806@inverse.ca>
|
||||
MIME-Version: 1.0
|
||||
To: message1to@cyril.dev
|
||||
CC: 2message1cc@cyril.dev, user10@cyril.dev
|
||||
Subject: message1subject
|
||||
Content-Type: text/plain; charset=us-ascii; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Reply-To: support@inverse.ca
|
||||
|
||||
Hello Jacques,
|
||||
|
||||
Can you read me?
|
||||
|
||||
--
|
||||
Cyril <cyril@cyril.dev>
|
||||
"""
|
||||
|
||||
message2 = """Return-Path: <cyril@cyril.dev>
|
||||
Received: from cyril.dev (localhost [127.0.0.1])
|
||||
by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA;
|
||||
Tue, 09 Dec 2009 07:42:16 -0400
|
||||
Message-ID: <410sepAC1F296.5060801a@cyril.dev>
|
||||
Date: Tue, 10 Sep 2009 07:42:14 -0400
|
||||
User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605)
|
||||
MIME-Version: 1.0
|
||||
From: Cyril <message2from@cyril.dev>
|
||||
To: message2to@cyril.dev
|
||||
CC: 3message2cc@cyril.dev
|
||||
Subject: message2subject
|
||||
Content-Type: text/plain; charset=us-ascii; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Reply-To: support@inverse.ca
|
||||
|
||||
Hello Jacques,
|
||||
|
||||
Can you read me?
|
||||
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
Stuff StuffStuffStuffStuff StuffStuffStuff StuffStuff
|
||||
--
|
||||
Cyril <cyril@cyril.dev>
|
||||
"""
|
||||
|
||||
message3 = """Return-Path: <cyril@cyril.dev>
|
||||
Received: from cyril.dev (localhost [127.0.0.1])
|
||||
by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA;
|
||||
Tue, 15 Dec 2009 07:42:16 -0400
|
||||
Message-ID: <4AC1aF2dec96.5060801a@cyril.dev>
|
||||
Date: Tue, 10 Dec 2009 07:42:14 -0400
|
||||
User-Agent: Thunderbird 2.0.0.22 (Macintosh/20090605)
|
||||
MIME-Version: 1.0
|
||||
From: Cyril <message3from@cyril.dev>
|
||||
To: message3to@cyril.dev
|
||||
CC: 1message3cc@cyril.dev
|
||||
Subject: Hallo
|
||||
Content-Type: text/plain; charset=us-ascii; format=flowed
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Reply-To: support@inverse.ca
|
||||
|
||||
Hello Jacques,
|
||||
|
||||
Can you read me?
|
||||
|
||||
This message is just a bit larger than message1 but smaller than message2
|
||||
--
|
||||
Cyril <cyril@cyril.dev>
|
||||
"""
|
||||
message1_received = """Received: from cyril.dev (localhost [127.0.0.1])
|
||||
by cyril.dev (Cyrus v2.3.8-Debian-2.3.8-1) with LMTPA;
|
||||
Tue, 17 Dec 2009 07:42:16 -0400"""
|
||||
|
||||
class DAVMailCollectionTest():
|
||||
resource = '/SOGo/dav/%s/Mail/' % username
|
||||
user_email = None
|
||||
|
||||
def setUp(self):
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
if self.user_email is None:
|
||||
self.user_email = fetchUserEmail(username)
|
||||
if self.user_email.startswith("mailto:"):
|
||||
self.user_email = self.user_email[7:]
|
||||
|
||||
self.resource = '/SOGo/dav/%s/Mail/%s_A_%s/' \
|
||||
% (username,
|
||||
username.replace("@", "_A_").replace(".", "_D_"),
|
||||
mailserver.replace(".", "_D_"))
|
||||
|
||||
## helper methods
|
||||
def _makeCollection(self, name, status = 201):
|
||||
url = "%s%s" % (self.resource, name)
|
||||
mkcol = webdavlib.WebDAVMKCOL(url)
|
||||
self.client.execute(mkcol)
|
||||
self.assertEquals(mkcol.response["status"], status,
|
||||
"failure creating collection"
|
||||
"(code = %d)" % mkcol.response["status"])
|
||||
|
||||
def _deleteCollection(self, name, status = 204):
|
||||
url = "%sfolder%s" % (self.resource, name)
|
||||
delete = webdavlib.WebDAVDELETE(url)
|
||||
self.client.execute(delete)
|
||||
self.assertEquals(delete.response["status"], status,
|
||||
"failure deleting collection"
|
||||
"(code = %d)" % delete.response["status"])
|
||||
|
||||
def _putMessage(self, client, folder, message,
|
||||
exp_status = 201):
|
||||
url = "%sfolder%s" % (self.resource, folder)
|
||||
put = webdavlib.HTTPPUT(url, message)
|
||||
put.content_type = "message/rfc822"
|
||||
client.execute(put)
|
||||
if (exp_status is not None):
|
||||
self.assertEquals(put.response["status"], exp_status,
|
||||
"message creation/modification:"
|
||||
" expected status code '%d' (received '%d')"
|
||||
% (exp_status, put.response["status"]))
|
||||
return put.response["headers"]["location"]
|
||||
|
||||
def _testProperty (self, url, property, expected, isDate = 0):
|
||||
propfind = webdavlib.WebDAVPROPFIND(url, (property, ), 0)
|
||||
self.client.execute(propfind)
|
||||
key = property.replace("{urn:schemas:httpmail:}", "a:")
|
||||
key = key.replace("{urn:schemas:mailheader:}", "a:")
|
||||
tmp = propfind.xpath_evaluate("{DAV:}response/{DAV:}propstat/{DAV:}prop")
|
||||
prop = tmp[0].firstChild;
|
||||
result = None
|
||||
|
||||
if prop:
|
||||
result = prop._get_firstChild()._get_nodeValue()
|
||||
#print key, result
|
||||
|
||||
if isDate:
|
||||
tstruct = time.strptime (result, "%a, %d %b %Y %H:%M:%S %Z")
|
||||
result = int (time.mktime (tstruct))
|
||||
|
||||
self.assertEquals(result, expected,
|
||||
"failure in propfind"
|
||||
"(%s != %s)" % (result, expected))
|
||||
|
||||
def testMKCOL(self):
|
||||
"""Folder creation"""
|
||||
self._deleteCollection("test-dav-mail-%40-abc")
|
||||
self._deleteCollection("test-dav-mail-@-def")
|
||||
self._deleteCollection("test-dav-mail-%20-ghi")
|
||||
|
||||
self._makeCollection("test-dav-mail-%40-abc")
|
||||
self._makeCollection("test-dav-mail-@-def")
|
||||
self._makeCollection("test-dav-mail-%20-ghi")
|
||||
self._makeCollection("test-dav-mail-%25-jkl", 500)
|
||||
|
||||
# Test MOVE
|
||||
# self._makeCollection ("test-dav-mail-movable")
|
||||
# url = "%sfolder%s" % (self.resource, "test-dav-mail-movable")
|
||||
# move = webdavlib.WebDAVMOVE (url)
|
||||
# move.destination = "http://cyril.dev%s%s2" % (self.resource, "test-dav-mail-movable")
|
||||
# move.host = "cyril.dev"
|
||||
# self.client.execute (move)
|
||||
# self.assertEquals(move.response["status"], 204,
|
||||
# "failure creating collection"
|
||||
# "(code = %d)" % move.response["status"])
|
||||
|
||||
def testPUT(self):
|
||||
"""Message creation"""
|
||||
self._deleteCollection("test-dav-mail")
|
||||
self._makeCollection("test-dav-mail")
|
||||
|
||||
# message creation on collection url
|
||||
url = "%s%s" % (self.resource, "foldertest-dav-mail/")
|
||||
put = webdavlib.HTTPPUT(url, message1)
|
||||
put.content_type = "message/rfc822"
|
||||
self.client.execute(put)
|
||||
self.assertEquals(put.response["status"], 201,
|
||||
"failure putting message"
|
||||
"(code = %d)" % put.response["status"])
|
||||
|
||||
itemLocation = put.response["headers"]["location"]
|
||||
get = webdavlib.WebDAVGET(itemLocation)
|
||||
self.client.execute(get)
|
||||
self.assertEquals(get.response["status"], 200,
|
||||
"failure getting item"
|
||||
"(code = %d)" % get.response["status"])
|
||||
|
||||
# message creation with explicit filename
|
||||
url = "%s%s" %(self.resource, "foldertest-dav-mail/blabla.eml")
|
||||
put = webdavlib.HTTPPUT(url, message1)
|
||||
put.content_type = "message/rfc822"
|
||||
self.client.execute(put)
|
||||
self.assertEquals(put.response["status"], 201,
|
||||
"failure putting message"
|
||||
"(code = %d)" % put.response["status"])
|
||||
|
||||
itemLocation = put.response["headers"]["location"]
|
||||
get = webdavlib.WebDAVGET(itemLocation)
|
||||
self.client.execute(get)
|
||||
self.assertEquals(get.response["status"], 200,
|
||||
"failure getting item"
|
||||
"(code = %d)" % get.response["status"])
|
||||
|
||||
self._deleteCollection("test-dav-mail")
|
||||
|
||||
def _testFilters(self, filters):
|
||||
for filter in filters:
|
||||
self._testFilter(filter)
|
||||
|
||||
def _testFilter(self, filter):
|
||||
expected_hrefs = {}
|
||||
expected_count = len(filter[1])
|
||||
for href in filter[1]:
|
||||
expected_hrefs[href] = True
|
||||
|
||||
received_count = 0
|
||||
url = "%sfolder%s" % (self.resource, "test-dav-mail")
|
||||
query = webdavlib.MailDAVMailQuery(url, ["displayname"], filter[0])
|
||||
self.client.execute(query)
|
||||
self.assertEquals(query.response["status"], 207,
|
||||
"filter %s:\n\tunexpected status: %d"
|
||||
% (filter[0], query.response["status"]))
|
||||
response_nodes = query.xpath_evaluate("{DAV:}response")
|
||||
for response_node in response_nodes:
|
||||
href_node = query.xpath_evaluate("{DAV:}href", response_node)[0]
|
||||
href = href_node.childNodes[0].nodeValue
|
||||
received_count = received_count + 1
|
||||
self.assertTrue(expected_hrefs.has_key(href),
|
||||
"filter %s:\n\tunexpected href: %s" % (filter[0], href))
|
||||
|
||||
self.assertEquals(len(filter[1]), received_count,
|
||||
"filter %s:\n\tunexpected amount of refs: %d"
|
||||
% (filter[0], received_count))
|
||||
|
||||
def _testSort(self, sortOrder, ascending = True):
|
||||
expected_hrefs = sortOrder[1]
|
||||
expected_count = len(expected_hrefs)
|
||||
|
||||
received_count = 0
|
||||
url = "%sfolder%s" % (self.resource, "test-dav-mail")
|
||||
query = webdavlib.MailDAVMailQuery(url, ["displayname"],
|
||||
None, sortOrder[0], ascending)
|
||||
self.client.execute(query)
|
||||
self.assertEquals(query.response["status"], 207,
|
||||
"sortOrder %s:\n\tunexpected status: %d"
|
||||
% (sortOrder[0], query.response["status"]))
|
||||
response_nodes = query.response["document"].findall("{DAV:}response")
|
||||
for response_node in response_nodes:
|
||||
href_node = response_node.find("{DAV:}href")
|
||||
href = href_node.text
|
||||
self.assertEquals(expected_hrefs[received_count], href,
|
||||
"sortOrder %s:\n\tunexpected href: %s (expecting: %s)"
|
||||
% (sortOrder[0], href,
|
||||
expected_hrefs[received_count]))
|
||||
received_count = received_count + 1
|
||||
|
||||
self.assertEquals(expected_count, received_count,
|
||||
"sortOrder %s:\n\tunexpected amount of refs: %d"
|
||||
% (sortOrder[0], received_count))
|
||||
|
||||
def testREPORTMailQueryFilters(self):
|
||||
"""mail-query filters"""
|
||||
self._deleteCollection("test-dav-mail")
|
||||
self._makeCollection("test-dav-mail")
|
||||
|
||||
msg1Loc = self._putMessage(self.client, "test-dav-mail", message1)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg1Loc)
|
||||
msg1Path = parsed.path
|
||||
msg2Loc = self._putMessage(self.client, "test-dav-mail", message2)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg2Loc)
|
||||
msg2Path = parsed.path
|
||||
msg3Loc = self._putMessage(self.client, "test-dav-mail", message3)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg3Loc)
|
||||
msg3Path = parsed.path
|
||||
|
||||
properties = ["{DAV:}displayname"]
|
||||
|
||||
## 1. test filter: receive-date
|
||||
# SINCE, BEFORE, ON
|
||||
# q = MailDAVMailQuery(self.resource, properties, filters = None)
|
||||
|
||||
filters = (({ "receive-date": { "from": "20091201T000000Z",
|
||||
"to": "20091208T000000Z" } },
|
||||
[]),
|
||||
({ "receive-date": { "from": "20091208T000000Z",
|
||||
"to": "20091213T134300Z" } },
|
||||
[ msg2Loc ]),
|
||||
({ "receive-date": { "from": "20091208T000000Z",
|
||||
"to": "20091216T134300Z" } },
|
||||
[ msg2Loc, msg3Loc ]),
|
||||
({ "receive-date": { "from": "20091216T000000Z",
|
||||
"to": "20091220T134300Z" } },
|
||||
[ msg1Loc ]),
|
||||
({ "receive-date": { "from": "20091220T000000Z",
|
||||
"to": "20091229T134300Z" } },
|
||||
[]))
|
||||
# receive-date seems to be considered the same as date by imapd
|
||||
print "Warning, receive-date test disabled"
|
||||
#self._testFilters(filters)
|
||||
|
||||
## 1. test filter: date
|
||||
# SENTSINCE, SENTBEFORE, SENTON
|
||||
|
||||
filters = (({ "date": { "from": "20090101T000000Z",
|
||||
"to": "20090201T000000Z" } },
|
||||
[]),
|
||||
({ "date": { "from": "20090912T000000Z",
|
||||
"to": "20090929T134300Z" } },
|
||||
[ msg1Loc ]),
|
||||
({ "date": { "from": "20090929T134300Z",
|
||||
"to": "20091209T000000Z" } },
|
||||
[]),
|
||||
({ "date": { "from": "20090901T134300Z",
|
||||
"to": "20091209T000000Z" } },
|
||||
[ msg1Loc, msg2Loc ]),
|
||||
({ "date": { "from": "20091201T000000Z",
|
||||
"to": "20091211T000000Z" } },
|
||||
[ msg3Loc ]),
|
||||
({ "date": { "from": "20091211T000000Z",
|
||||
"to": "20101211T000000Z" } },
|
||||
[]),
|
||||
({ "date": { "from": "20090101T000000Z",
|
||||
"to": "20100101T000000Z" } },
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: sequence
|
||||
# x:y
|
||||
filters = (({ "sequence": { "from": "1" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "sequence": { "from": "5" }},
|
||||
[]),
|
||||
({ "sequence": { "to": "5" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "sequence": { "from": "1",
|
||||
"to": "2" }},
|
||||
[ msg1Loc, msg2Loc ]))
|
||||
# Sequence not yet implemented
|
||||
print "Warning, sequence test disabled"
|
||||
#self._testFilters(filters)
|
||||
|
||||
## 1. test filter: uid
|
||||
# UID
|
||||
filters = (({ "uid": { "from": "1" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
# disabled because we get 3
|
||||
#({ "uid": { "from": "5" }},
|
||||
# []),
|
||||
({ "uid": { "to": "5" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "uid": { "from": "1",
|
||||
"to": "2" }},
|
||||
[ msg1Loc, msg2Loc ]))
|
||||
print "Warning, one of the uid tests is disabled"
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: from
|
||||
# FROM
|
||||
filters = (({ "from": { "match": "message" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "from": { "match": "Cyril" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "from": { "match": "cyril.dev" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "from": { "match": "message1from" }},
|
||||
[ msg1Loc ]),
|
||||
({ "from": { "match": "message2from" }},
|
||||
[ msg2Loc ]),
|
||||
({ "from": { "match": "message3from" }},
|
||||
[ msg3Loc ]))
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: to
|
||||
# TO
|
||||
filters = (({ "to": { "match": "message" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "to": { "match": "Cyril" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "to": { "match": "cyril.dev" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "to": { "match": "message1to" }},
|
||||
[ msg1Loc ]),
|
||||
({ "to": { "match": "message2to" }},
|
||||
[ msg2Loc ]),
|
||||
({ "to": { "match": "message3to" }},
|
||||
[ msg3Loc ]))
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: cc
|
||||
# CC
|
||||
filters = (({ "cc": { "match": "message" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "cc": { "match": "Cyril" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "cc": { "match": "cyril.dev" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "cc": { "match": "message1cc" }},
|
||||
[ msg1Loc ]),
|
||||
({ "cc": { "match": "message2cc" }},
|
||||
[ msg2Loc ]),
|
||||
({ "cc": { "match": "message3cc" }},
|
||||
[ msg3Loc ]))
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: bcc
|
||||
# BCC
|
||||
## TODO
|
||||
|
||||
## 1. test filter: body
|
||||
# BODY
|
||||
filters = (({ "body": { "match": "Hello" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "body": { "match": "Stuff" }},
|
||||
[ msg2Loc ]),
|
||||
({ "body": { "match": "DOESNOT MATCH" }},
|
||||
[]))
|
||||
self._testFilters(filters)
|
||||
|
||||
## 1. test filter: size
|
||||
# LARGER, SMALLER
|
||||
#1 848
|
||||
#2 4308
|
||||
#3 699
|
||||
filters = (({ "size": { "min": "300",
|
||||
"max": "300" }},
|
||||
[]),
|
||||
({ "size": { "min": "800",
|
||||
"max": "800" }},
|
||||
[]),
|
||||
({ "size": { "min": "5000",
|
||||
"max": "5000" }},
|
||||
[]),
|
||||
({ "size": { "min": "838",
|
||||
"max": "838" }},
|
||||
[ msg1Loc ]),
|
||||
({ "size": { "min": "699",
|
||||
"max": "4308" }},
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]),
|
||||
({ "size": { "min": "700",
|
||||
"max": "4308" }},
|
||||
[ msg1Loc, msg2Loc ]),
|
||||
({ "size": { "min": "698",
|
||||
"max": "848" }},
|
||||
[ msg1Loc, msg3Loc ]),
|
||||
({ "size": { "min": "300",
|
||||
"max": "5000" },
|
||||
"size": { "min": "840",
|
||||
"max": "850",
|
||||
"not": "true" }},
|
||||
[ msg2Loc, msg3Loc ]))
|
||||
|
||||
print "message flags are not handled yet"
|
||||
## 1. test filter: answered
|
||||
# ANSWERED, UNANSWERED
|
||||
## 1. test filter: draft
|
||||
# DRAFT
|
||||
## 1. test filter: flagged
|
||||
# FLAGGED
|
||||
## 1. test filter: recent
|
||||
# RECENT
|
||||
## 1. test filter: seen
|
||||
# SEEN
|
||||
## 1. test filter: deleted
|
||||
# DELETED
|
||||
## 1. test filter: keywords
|
||||
# KEYWORD x
|
||||
|
||||
## 1. test filter: multiple combinations
|
||||
filters = (({ "body": { "match": "Hello" },
|
||||
"cc": { "match": "message1cc" }},
|
||||
[ msg1Loc ]),
|
||||
({ "to": { "match": "message" },
|
||||
"uid": { "from": "1",
|
||||
"to": "2" }},
|
||||
[ msg1Loc, msg2Loc ]),
|
||||
({ "to": { "match": "message" },
|
||||
"uid": { "from": "1",
|
||||
"to": "2" },
|
||||
"cc": { "match": "message3cc" }},
|
||||
[]))
|
||||
self._testFilters(filters)
|
||||
|
||||
self._deleteCollection("test-dav-mail")
|
||||
|
||||
def testREPORTMailQuerySort(self):
|
||||
"""mail-query sort"""
|
||||
self._deleteCollection("test-dav-mail")
|
||||
self._makeCollection("test-dav-mail")
|
||||
|
||||
msg1Loc = self._putMessage(self.client, "test-dav-mail", message1)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg1Loc)
|
||||
msg1Path = parsed.path
|
||||
msg2Loc = self._putMessage(self.client, "test-dav-mail", message2)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg2Loc)
|
||||
msg2Path = parsed.path
|
||||
msg3Loc = self._putMessage(self.client, "test-dav-mail", message3)
|
||||
parsed = webdavlib.HTTPUnparsedURL(msg3Loc)
|
||||
msg3Path = parsed.path
|
||||
|
||||
# 1. test sort: (receive-date) ARRIVAL
|
||||
self._testSort(([ "{urn:schemas:mailheader:}received" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: (date) DATE
|
||||
self._testSort(([ "{urn:schemas:mailheader:}date" ],
|
||||
[ msg2Loc, msg1Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: FROM
|
||||
self._testSort(([ "{urn:schemas:mailheader:}from" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: TO
|
||||
self._testSort(([ "{urn:schemas:mailheader:}to" ],
|
||||
[ msg1Loc, msg2Loc, msg3Loc ]))
|
||||
|
||||
# 1. test sort: CC
|
||||
self._testSort(([ "{urn:schemas:mailheader:}cc" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: SUBJECT
|
||||
self._testSort(([ "{DAV:}displayname" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
self._testSort(([ "{urn:schemas:mailheader:}subject" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: SIZE
|
||||
self._testSort(([ "{DAV:}getcontentlength" ],
|
||||
[ msg3Loc, msg1Loc, msg2Loc ]))
|
||||
|
||||
# 1. test sort: REVERSE CC
|
||||
self._testSort(([ "{urn:schemas:mailheader:}cc" ],
|
||||
[ msg2Loc, msg1Loc, msg3Loc ]),
|
||||
False)
|
||||
|
||||
self._deleteCollection("test-dav-mail")
|
||||
|
||||
def testPROPFIND(self):
|
||||
"""Message properties"""
|
||||
self._deleteCollection ("test-dav-mail")
|
||||
self._makeCollection ("test-dav-mail")
|
||||
|
||||
url = "%s%s" % (self.resource, "foldertest-dav-mail/")
|
||||
put = webdavlib.HTTPPUT (url, message1)
|
||||
put.content_type = "message/rfc822"
|
||||
self.client.execute (put)
|
||||
self.assertEquals(put.response["status"], 201,
|
||||
"failure putting message"
|
||||
"(code = %d)" % put.response["status"])
|
||||
|
||||
itemLocation = put.response["headers"]["location"]
|
||||
tests = (("{urn:schemas:httpmail:}date", 1254156134, 1),
|
||||
("{urn:schemas:httpmail:}hasattachment", "0", 0),
|
||||
("{urn:schemas:httpmail:}read", "0", 0),
|
||||
("{urn:schemas:httpmail:}textdescription",
|
||||
"<![CDATA[%s]]>" % message1, 0),
|
||||
("{urn:schemas:httpmail:}unreadcount", None, 0),
|
||||
("{urn:schemas:mailheader:}cc",
|
||||
"2message1cc@cyril.dev, user10@cyril.dev", 0),
|
||||
("{urn:schemas:mailheader:}date",
|
||||
"Mon, 28 Sep 2009 11:42:14 GMT", 0),
|
||||
("{urn:schemas:mailheader:}from",
|
||||
"Cyril <message1from@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}in-reply-to", None, 0),
|
||||
("{urn:schemas:mailheader:}message-id",
|
||||
"<4AC1F29sept6.5060801@cyril.dev>", 0),
|
||||
("{urn:schemas:mailheader:}received", message1_received, 0),
|
||||
("{urn:schemas:mailheader:}references",
|
||||
"<4AC3BF1B.3010806@inverse.ca>", 0),
|
||||
("{urn:schemas:mailheader:}subject", "message1subject", 0),
|
||||
("{urn:schemas:mailheader:}to", "message1to@cyril.dev", 0))
|
||||
|
||||
for test in tests:
|
||||
property, expected, isDate = test
|
||||
self._testProperty(itemLocation, property, expected, isDate)
|
||||
|
||||
self._deleteCollection ("test-dav-mail")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
from config import hostname, port, username, password, white_listed_attendee
|
||||
|
||||
import preferences
|
||||
import simplejson
|
||||
import sogotests
|
||||
import unittest
|
||||
import utilities
|
||||
|
||||
class preferencesTest(unittest.TestCase):
|
||||
|
||||
def _setTextPref(self, prefText = None ):
|
||||
""" set a text preference to a known value """
|
||||
self.prefs.set("autoReplyText", prefText)
|
||||
|
||||
# make sure it was set correctly
|
||||
prefData = self.prefs.get("Vacation")
|
||||
|
||||
self.assertEqual(prefData["autoReplyText"], prefText,
|
||||
"%s != %s" % (prefData["autoReplyText"], prefText))
|
||||
|
||||
def setUp(self):
|
||||
self.prefs = preferences.preferences()
|
||||
|
||||
def tearDown(self):
|
||||
self.prefs.set("autoReplyText", "")
|
||||
|
||||
def testSetTextPreferences(self):
|
||||
""" Set/get a text preference - normal characters"""
|
||||
|
||||
self._setTextPref("defaultText")
|
||||
|
||||
def testSetTextPreferencesWeirdChars(self):
|
||||
""" Set/get a text preference - weird characters - used to crash on 1.3.12"""
|
||||
prefText = "weird data \ ' \"; ^"
|
||||
self._setTextPref(prefText)
|
||||
|
||||
def testSetPreventInvitation(self):
|
||||
""" Set/get the PreventInvitation pref"""
|
||||
self.prefs.set('PreventInvitations', '0')
|
||||
notset = self.prefs.get_settings('')['Calendar']['PreventInvitations']
|
||||
self.assertEqual(notset, 0)
|
||||
self.prefs.set('enablePreventInvitations', '0')
|
||||
isset = self.prefs.get_settings('')['Calendar']['PreventInvitations']
|
||||
self.assertEqual(isset, 1)
|
||||
|
||||
def testPreventInvitationsWhiteList(self):
|
||||
"""Add to the PreventInvitations Whitelist"""
|
||||
self.prefs.set("whiteList", simplejson.dumps(white_listed_attendee))
|
||||
whitelist = self.prefs.get_settings('Calendar')['PreventInvitationsWhitelist']
|
||||
self.assertEqual(whitelist, white_listed_attendee)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,209 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
from config import hostname, port, username, password, \
|
||||
superuser, superuser_password, \
|
||||
attendee1, attendee1_username, \
|
||||
attendee1_password, \
|
||||
attendee1_delegate, attendee1_delegate_username, \
|
||||
attendee1_delegate_password, \
|
||||
resource_no_overbook, resource_can_overbook, \
|
||||
white_listed_attendee
|
||||
|
||||
import preferences
|
||||
import simplejson
|
||||
import sogotests
|
||||
import unittest
|
||||
import utilities
|
||||
import datetime
|
||||
import dateutil.tz
|
||||
import vobject
|
||||
import vobject.base
|
||||
import vobject.icalendar
|
||||
import webdavlib
|
||||
import StringIO
|
||||
|
||||
|
||||
class preventInvitationsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.prefs = preferences.preferences(attendee1, attendee1_password)
|
||||
self.caldav = CalDAVSchedulingTest(self)
|
||||
|
||||
def tearDown(self):
|
||||
self.prefs.set("autoReplyText", "")
|
||||
self.prefs.set('PreventInvitations', '0')
|
||||
self.prefs.set("whiteList", "")
|
||||
#- Manual Cleanup, not called because classs is not derived from unittest
|
||||
self.caldav.tearDown()
|
||||
|
||||
def testDontPreventInvitation(self):
|
||||
""" Set/get the PreventInvitation pref"""
|
||||
#- First accept the invitation
|
||||
self.prefs.set('PreventInvitations', '0')
|
||||
notset = self.prefs.get_settings('')['Calendar']['PreventInvitations']
|
||||
self.assertEqual(notset, 0)
|
||||
self.caldav.AddAttendee()
|
||||
self.caldav.VerifyEvent()
|
||||
|
||||
def testPreventInvitation(self):
|
||||
""" Set PreventInvitation and don't accept the Invitation"""
|
||||
#- Second, enable PreventInviation and refuse it
|
||||
self.prefs.set('enablePreventInvitations', '0')
|
||||
isset = self.prefs.get_settings('')['Calendar']['PreventInvitations']
|
||||
self.assertEqual(isset, 1)
|
||||
self.caldav.AddAttendee(409)
|
||||
self.caldav.VerifyEvent(404)
|
||||
|
||||
def testPreventInvitationWhiteList(self):
|
||||
""" Set PreventInvitation add to WhiteList and accept the Invitation"""
|
||||
#- First, add the Organiser to the Attendee's whitelist
|
||||
self.prefs.set('enablePreventInvitations', '0')
|
||||
self.prefs.set("whiteList", simplejson.dumps(white_listed_attendee))
|
||||
whitelist = self.prefs.get_settings('Calendar')['PreventInvitationsWhitelist']
|
||||
self.assertEqual(whitelist, white_listed_attendee)
|
||||
|
||||
#- Second, try again to invite, it should work
|
||||
self.prefs.set('enablePreventInvitations', '0')
|
||||
isset = self.prefs.get_settings('')['Calendar']['PreventInvitations']
|
||||
self.assertEqual(isset, 1)
|
||||
self.caldav.AddAttendee()
|
||||
self.caldav.VerifyEvent()
|
||||
|
||||
|
||||
class CalDAVSchedulingTest(object):
|
||||
def __init__(self, parent_self):
|
||||
self.test = parent_self # used for utilities
|
||||
self.setUp()
|
||||
|
||||
def setUp(self):
|
||||
self.superuser_client = webdavlib.WebDAVClient(hostname, port,
|
||||
superuser, superuser_password)
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
self.attendee1_client = webdavlib.WebDAVClient(hostname, port,
|
||||
attendee1_username, attendee1_password)
|
||||
self.attendee1_delegate_client = webdavlib.WebDAVClient(hostname, port,
|
||||
attendee1_delegate_username, attendee1_delegate_password)
|
||||
|
||||
utility = utilities.TestUtility(self.test, self.client)
|
||||
(self.user_name, self.user_email) = utility.fetchUserInfo(username)
|
||||
(self.attendee1_name, self.attendee1_email) = utility.fetchUserInfo(attendee1)
|
||||
(self.attendee1_delegate_name, self.attendee1_delegate_email) = utility.fetchUserInfo(attendee1_delegate)
|
||||
|
||||
self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username
|
||||
self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1
|
||||
self.attendee1_delegate_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1_delegate
|
||||
|
||||
# fetch non existing event to let sogo create the calendars in the db
|
||||
self._getEvent(self.client, "%snonexistent" % self.user_calendar, exp_status=404)
|
||||
self._getEvent(self.attendee1_client, "%snonexistent" % self.attendee1_calendar, exp_status=404)
|
||||
self._getEvent(self.attendee1_delegate_client, "%snonexistent" %
|
||||
self.attendee1_delegate_calendar, exp_status=404)
|
||||
|
||||
# list of ics used by the test.
|
||||
# tearDown will loop over this and wipe them in all users' calendar
|
||||
self.ics_list = []
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# delete all created events from all users' calendar
|
||||
for ics in self.ics_list:
|
||||
self._deleteEvent(self.superuser_client,
|
||||
"%s%s" % (self.user_calendar, ics), None)
|
||||
self._deleteEvent(self.superuser_client,
|
||||
"%s%s" % (self.attendee1_calendar, ics), None)
|
||||
self._deleteEvent(self.superuser_client,
|
||||
"%s%s" % (self.attendee1_delegate_calendar, ics), None)
|
||||
|
||||
def _newEvent(self, summary="test event", uid="test", transp=0):
|
||||
transparency = ("OPAQUE", "TRANSPARENT")
|
||||
|
||||
newCal = vobject.iCalendar()
|
||||
vevent = newCal.add('vevent')
|
||||
vevent.add('summary').value = summary
|
||||
vevent.add('transp').value = transparency[transp]
|
||||
|
||||
now = datetime.datetime.now(dateutil.tz.gettz("America/Montreal"))
|
||||
startdate = vevent.add('dtstart')
|
||||
startdate.value = now
|
||||
enddate = vevent.add('dtend')
|
||||
enddate.value = now + datetime.timedelta(0, 3600)
|
||||
vevent.add('uid').value = uid
|
||||
vevent.add('dtstamp').value = now
|
||||
vevent.add('last-modified').value = now
|
||||
vevent.add('created').value = now
|
||||
vevent.add('class').value = "PUBLIC"
|
||||
vevent.add('sequence').value = "0"
|
||||
|
||||
return newCal
|
||||
|
||||
def _putEvent(self, client, filename, event, exp_status = 201):
|
||||
put = webdavlib.HTTPPUT(filename, event.serialize())
|
||||
put.content_type = "text/calendar; charset=utf-8"
|
||||
client.execute(put)
|
||||
if exp_status is not None:
|
||||
self.test.assertEquals(put.response["status"], exp_status)
|
||||
|
||||
def _getEvent(self, client, filename, exp_status = 200):
|
||||
get = webdavlib.HTTPGET(filename)
|
||||
client.execute(get)
|
||||
|
||||
if exp_status is not None:
|
||||
self.test.assertEquals(get.response["status"], exp_status)
|
||||
|
||||
if get.response["headers"]["content-type"].startswith("text/calendar"):
|
||||
stream = StringIO.StringIO(get.response["body"])
|
||||
event = vobject.base.readComponents(stream).next()
|
||||
else:
|
||||
event = None
|
||||
|
||||
return event
|
||||
|
||||
def _deleteEvent(self, client, filename, exp_status = 204):
|
||||
delete = webdavlib.WebDAVDELETE(filename)
|
||||
client.execute(delete)
|
||||
if exp_status is not None:
|
||||
self.test.assertEquals(delete.response["status"], exp_status)
|
||||
|
||||
def AddAttendee(self, exp_status=204):
|
||||
""" add attendee after event creation """
|
||||
|
||||
# make sure the event doesn't exist
|
||||
ics_name = "test-add-attendee.ics"
|
||||
self.ics_list += [ics_name]
|
||||
|
||||
self._deleteEvent(self.client,
|
||||
"%s%s" % (self.user_calendar,ics_name), None)
|
||||
self._deleteEvent(self.attendee1_client,
|
||||
"%s%s" % (self.attendee1_calendar,ics_name), None)
|
||||
|
||||
# 1. create an event in the organiser's calendar
|
||||
event = self._newEvent(summary="Test add attendee", uid="Test add attendee")
|
||||
organizer = event.vevent.add('organizer')
|
||||
organizer.cn_param = self.user_name
|
||||
organizer.value = self.user_email
|
||||
self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event)
|
||||
|
||||
# 2. add an attendee
|
||||
event.add("method").value = "REQUEST"
|
||||
attendee = event.vevent.add('attendee')
|
||||
attendee.cn_param = self.attendee1_name
|
||||
attendee.rsvp_param = "TRUE"
|
||||
attendee.partstat_param = "NEEDS-ACTION"
|
||||
attendee.value = self.attendee1_email
|
||||
self._putEvent(self.client, "%s%s" % (self.user_calendar, ics_name), event,
|
||||
exp_status=exp_status)
|
||||
|
||||
#- save event for VerifyEvent
|
||||
self.event = event
|
||||
self.ics_name = ics_name
|
||||
|
||||
def VerifyEvent(self, exp_status=200):
|
||||
# 1. verify that the attendee has the event
|
||||
attendee_event = self._getEvent(self.attendee1_client, "%s%s" % (self.attendee1_calendar, self.ics_name), exp_status)
|
||||
|
||||
# 2. make sure the received event match the original one
|
||||
if attendee_event:
|
||||
self.test.assertEquals(self.event.vevent.uid, attendee_event.vevent.uid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
from config import hostname, port, username, password, sieve_port, sieve_server
|
||||
|
||||
import managesieve
|
||||
import preferences
|
||||
import sogotests
|
||||
import unittest
|
||||
import utilities
|
||||
import webdavlib
|
||||
|
||||
sieve_simple_vacation="""require ["vacation"];\r\nvacation :days %(days)d :addresses ["%(mailaddr)s"] text:\r\n%(vacation_msg)s\r\n.\r\n;\r\n"""
|
||||
sieve_vacation_ignoreLists="""require ["vacation"];\r\nif allof ( not exists ["list-help", "list-unsubscribe", "list-subscribe", "list-owner", "list-post", "list-archive", "list-id", "Mailing-List"], not header :comparator "i;ascii-casemap" :is "Precedence" ["list", "bulk", "junk"], not header :comparator "i;ascii-casemap" :matches "To" "Multiple recipients of*" ) { vacation :days %(days)d :addresses ["%(mailaddr)s"] text:\r\n%(vacation_msg)s\r\n.\r\n;\r\n}\r\n"""
|
||||
sieve_simple_forward="""redirect "%(redirect_mailaddr)s";\r\n"""
|
||||
sieve_forward_keep="""redirect "%(redirect_mailaddr)s";\r\nkeep;\r\n"""
|
||||
sieve_simple_filter="""require ["fileinto"];\r\nif anyof (header :contains "subject" "%(subject)s") {\r\n fileinto "%(folderName)s";\r\n}\r\n"""
|
||||
|
||||
class sieveTest(unittest.TestCase):
|
||||
def _killFilters(self):
|
||||
filtersKill={}
|
||||
# kill existing filters
|
||||
filtersKill["SOGoSieveFilters"]= "[]"
|
||||
# vacation filters
|
||||
filtersKill["autoReplyText"] = ""
|
||||
filtersKill["autoReplyEmailAddresses"] = ""
|
||||
# forwarding filters
|
||||
filtersKill["forwardAddress"] = ""
|
||||
|
||||
self.prefs=preferences.preferences()
|
||||
self.prefs.set(filtersKill)
|
||||
|
||||
def setUp(self):
|
||||
ret = ""
|
||||
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
utility = utilities.TestUtility(self, self.client)
|
||||
(self.user_name, self.user_email) = utility.fetchUserInfo(username)
|
||||
self.user_email = self.user_email.replace("mailto:", "")
|
||||
|
||||
self.ms = managesieve.MANAGESIEVE(sieve_server, sieve_port)
|
||||
self.assertEqual(self.ms.login("", username, password), "OK",
|
||||
"Couldn't login")
|
||||
|
||||
self._killFilters()
|
||||
|
||||
def tearDown(self):
|
||||
self._killFilters()
|
||||
|
||||
def _getSogoSieveScript(self):
|
||||
sieveFoundsogo=0
|
||||
createdSieveScript=""
|
||||
(ret, sieveScriptList) = self.ms.listscripts()
|
||||
self.assertEqual(ret, "OK", "Couldn't get sieve script list")
|
||||
|
||||
for (script, isActive) in sieveScriptList:
|
||||
if (script == "sogo"):
|
||||
sieveFoundsogo=1
|
||||
self.assertEqual(isActive, True, "sogo sieve script is not active!")
|
||||
(ret, createdSieveScript) = self.ms.getscript(script)
|
||||
self.assertEqual(ret, "OK", "Couldn't get sogo sieve script")
|
||||
|
||||
self.assertEqual(sieveFoundsogo, 1, "sogo sieve script not found!")
|
||||
|
||||
return createdSieveScript
|
||||
|
||||
def testSieveSimpleVacation(self):
|
||||
""" enable simple vacation script """
|
||||
vacation_msg="vacation test"
|
||||
daysSelect=3
|
||||
|
||||
sieveScript = sieve_simple_vacation % { "mailaddr": self.user_email,
|
||||
"vacation_msg": vacation_msg,
|
||||
"days": preferences.daysBetweenResponseList[daysSelect],
|
||||
}
|
||||
|
||||
filterAdd = {"Vacation":"1",
|
||||
"autoReplyText": vacation_msg,
|
||||
"daysBetweenResponse": "%d" % daysSelect,
|
||||
"autoReplyEmailAddresses": self.user_email,
|
||||
}
|
||||
|
||||
self.prefs.set(filterAdd)
|
||||
createdSieveScript=self._getSogoSieveScript()
|
||||
|
||||
self.assertEqual(sieveScript, createdSieveScript)
|
||||
|
||||
def testSieveVacationIgnoreLists(self):
|
||||
""" enable vacation script - ignore lists"""
|
||||
vacation_msg="vacation test - ignore list"
|
||||
daysSelect=2
|
||||
|
||||
sieveScript = sieve_vacation_ignoreLists % { "mailaddr": self.user_email,
|
||||
"vacation_msg": vacation_msg,
|
||||
"days": preferences.daysBetweenResponseList[daysSelect],
|
||||
}
|
||||
|
||||
filterAdd = {"Vacation":"1",
|
||||
"autoReplyText": vacation_msg,
|
||||
"daysBetweenResponse": "%d" % daysSelect,
|
||||
"autoReplyEmailAddresses": self.user_email,
|
||||
"ignoreLists": "1",
|
||||
}
|
||||
|
||||
self.prefs.set(filterAdd)
|
||||
createdSieveScript=self._getSogoSieveScript()
|
||||
|
||||
self.assertEqual(sieveScript, createdSieveScript)
|
||||
|
||||
def testSieveSimpleForward(self):
|
||||
""" enable simple forwarding """
|
||||
redirect_mailaddr="nonexistent@inverse.com"
|
||||
|
||||
sieveScript = sieve_simple_forward % { "redirect_mailaddr": redirect_mailaddr }
|
||||
|
||||
filterAdd = { "Forward": "1",
|
||||
"forwardAddress": redirect_mailaddr,
|
||||
}
|
||||
|
||||
self.prefs.set(filterAdd)
|
||||
createdSieveScript = self._getSogoSieveScript()
|
||||
self.assertEqual(sieveScript, createdSieveScript)
|
||||
|
||||
def testSieveForwardKeepCopy(self):
|
||||
""" enable email forwarding - keep a copy """
|
||||
redirect_mailaddr="nonexistent@inverse.com"
|
||||
|
||||
sieveScript = sieve_forward_keep % { "redirect_mailaddr": redirect_mailaddr }
|
||||
|
||||
filterAdd = { "Forward": "1",
|
||||
"forwardAddress": redirect_mailaddr,
|
||||
"keepCopy": "1",
|
||||
}
|
||||
|
||||
self.prefs.set(filterAdd)
|
||||
createdSieveScript = self._getSogoSieveScript()
|
||||
self.assertEqual(sieveScript, createdSieveScript)
|
||||
|
||||
def testSieveSimpleFilter(self):
|
||||
""" add simple sieve filter """
|
||||
folderName="Sent"
|
||||
subject=__name__
|
||||
|
||||
sieveScript=sieve_simple_filter % { "subject": subject, "folderName": folderName }
|
||||
|
||||
filterAdd = { "SOGoSieveFilters": """[{"active": true, "actions": [{"method": "fileinto", "argument": "Sent"}], "rules": [{"operator": "contains", "field": "subject", "value": "%s"}], "match": "any", "name": "%s"}]""" % (subject, folderName)
|
||||
}
|
||||
|
||||
self.prefs.set(filterAdd)
|
||||
createdSieveScript = self._getSogoSieveScript()
|
||||
self.assertEqual(sieveScript, createdSieveScript)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# XXX this script has to be run as root because it su to sogo_user
|
||||
# in order to use its .GNUstepDefaults prefs
|
||||
# Would be much better to have another way to specify which Defaults to use
|
||||
|
||||
from config import sogo_user, sogo_tool_path
|
||||
|
||||
import os
|
||||
import pwd
|
||||
import shutil
|
||||
import sogotests
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
class sogoToolTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.backupdir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir("/")
|
||||
shutil.rmtree(self.backupdir)
|
||||
|
||||
def testBackupAll(self):
|
||||
""" sogo-tool backup ALL """
|
||||
|
||||
(uid, gid) = pwd.getpwnam(sogo_user)[2:4]
|
||||
|
||||
# We need to run as root since there's no way
|
||||
# of using another user's GNUstepDefaults
|
||||
self.assertEqual(os.getuid(), 0, "this test must run as root...")
|
||||
|
||||
os.chown(self.backupdir, uid, gid)
|
||||
cmd = "sudo -u %s bash -c \"(cd %s && %s backup . ALL >/dev/null 2>&1)\"" % (sogo_user, self.backupdir, sogo_tool_path)
|
||||
#print "sogo-tool cmd to execute %s" % cmd
|
||||
status = os.system(cmd)
|
||||
#print "Exit status of os.system(): %d" % status
|
||||
rc = os.WEXITSTATUS(status)
|
||||
#self.assertEqual(rc, 0, "sogo-tool failed RC=%d" % rc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
|
||||
from config import hostname, port, username, password, \
|
||||
webCalendarURL
|
||||
|
||||
import simplejson
|
||||
import sogoLogin
|
||||
import sogotests
|
||||
import unittest
|
||||
import utilities
|
||||
import webdavlib
|
||||
import httplib
|
||||
|
||||
|
||||
class UIPostsTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.client = webdavlib.WebDAVClient(hostname, port)
|
||||
self.gcClient = webdavlib.WebDAVClient(hostname, port)
|
||||
self.cookie = sogoLogin.getAuthCookie(hostname, port, username, password)
|
||||
|
||||
def _urlPostData(self, client, url, data, exp_status=200):
|
||||
post = webdavlib.HTTPPOST(url, data)
|
||||
post.content_type = "application/x-www-form-urlencoded"
|
||||
post.cookie = self.cookie
|
||||
|
||||
client.execute(post)
|
||||
if (exp_status is not None):
|
||||
self.assertEquals(post.response["status"], exp_status)
|
||||
return post.response
|
||||
|
||||
def _urlGet(self, client, url, exp_status=200):
|
||||
get = webdavlib.HTTPGET(url)
|
||||
get.cookie = self.cookie
|
||||
|
||||
client.execute(get)
|
||||
if (exp_status is not None):
|
||||
self.assertEquals(get.response["status"], exp_status)
|
||||
return get.response
|
||||
|
||||
def testAddWebCalendar(self):
|
||||
""" Add Web Calendar """
|
||||
|
||||
ret=True
|
||||
data = "url=%s" % webCalendarURL
|
||||
calendarBaseURL="/SOGo/so/%s/Calendar" % username
|
||||
addWebCalendarURL = "%s/addWebCalendar" % calendarBaseURL
|
||||
response = self._urlPostData(self.client, addWebCalendarURL, data)
|
||||
|
||||
respJSON = simplejson.loads(response['body'])
|
||||
folderID = respJSON['folderID']
|
||||
|
||||
#sogo1:Calendar/C07-5006F300-1-370E2480
|
||||
(_, calID) = folderID.split('/', 1)
|
||||
self.assertNotEqual(calID, None)
|
||||
|
||||
# reload the cal
|
||||
calURL = "%s/%s" % (calendarBaseURL, calID)
|
||||
try:
|
||||
response = self._urlGet(self.client, "%s/reload" % calURL, exp_status=None)
|
||||
except httplib.BadStatusLine:
|
||||
# that's bad, the server probably reset the connection. fake a 502
|
||||
response['status'] = 502
|
||||
|
||||
# cleanup our trash
|
||||
self._urlPostData(self.gcClient, "%s/delete" % calURL, "", exp_status=None)
|
||||
|
||||
# delayed assert to allow cal deletion on failure
|
||||
self.assertEqual(response['status'], 200)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,155 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password, subscriber_username
|
||||
|
||||
import sogotests
|
||||
import unittest
|
||||
import utilities
|
||||
import webdavlib
|
||||
|
||||
class WebDAVTest(unittest.TestCase):
|
||||
def __init__(self, arg):
|
||||
unittest.TestCase.__init__(self, arg)
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
self.dav_utility = utilities.TestUtility(self, self.client)
|
||||
|
||||
def testPrincipalCollectionSet(self):
|
||||
"""property: 'principal-collection-set' on collection object"""
|
||||
resource = '/SOGo/dav/%s/' % username
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}principal-collection-set"],
|
||||
0)
|
||||
self.client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207)
|
||||
nodes = propfind.response["document"] \
|
||||
.findall('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href')
|
||||
responseHref = nodes[0].text
|
||||
if responseHref[0:4] == "http":
|
||||
self.assertEquals("http://%s/SOGo/dav/" % hostname, responseHref,
|
||||
"{DAV:}principal-collection-set returned %s instead of 'http../SOGo/dav/'"
|
||||
% ( responseHref, resource ))
|
||||
else:
|
||||
self.assertEquals("/SOGo/dav/", responseHref,
|
||||
"{DAV:}principal-collection-set returned %s instead of '/SOGo/dav/'"
|
||||
% responseHref)
|
||||
|
||||
def testPrincipalCollectionSet2(self):
|
||||
"""property: 'principal-collection-set' on non-collection object"""
|
||||
resource = '/SOGo/dav/%s/freebusy.ifb' % username
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}principal-collection-set"],
|
||||
0)
|
||||
self.client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207)
|
||||
node = propfind.response["document"] \
|
||||
.find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href')
|
||||
responseHref = node.text
|
||||
expectedHref = '/SOGo/dav/'
|
||||
if responseHref[0:4] == "http":
|
||||
self.assertEquals("http://%s%s" % (hostname, expectedHref), responseHref,
|
||||
"{DAV:}principal-collection-set returned %s instead of '%s'"
|
||||
% ( responseHref, expectedHref ))
|
||||
else:
|
||||
self.assertEquals(expectedHref, responseHref,
|
||||
"{DAV:}principal-collection-set returned %s instead of '%s'"
|
||||
% ( responseHref, expectedHref ))
|
||||
|
||||
def _testPropfindURL(self, resource):
|
||||
resourceWithSlash = resource[-1] == '/'
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["{DAV:}displayname", "{DAV:}resourcetype"],
|
||||
1)
|
||||
self.client.execute(propfind)
|
||||
self.assertEquals(propfind.response["status"], 207)
|
||||
|
||||
nodes = propfind.response["document"].findall('{DAV:}response')
|
||||
for node in nodes:
|
||||
responseHref = node.find('{DAV:}href').text
|
||||
hasSlash = responseHref[-1] == '/'
|
||||
resourcetype = node.find('{DAV:}propstat/{DAV:}prop/{DAV:}resourcetype')
|
||||
isCollection = len(resourcetype.getchildren()) > 0
|
||||
if isCollection:
|
||||
self.assertEquals(hasSlash, resourceWithSlash,
|
||||
"failure with href '%s' while querying '%s'"
|
||||
% (responseHref, resource))
|
||||
else:
|
||||
self.assertEquals(hasSlash, False,
|
||||
"failure with href '%s' while querying '%s'"
|
||||
% (responseHref, resource))
|
||||
|
||||
def testPropfindURL(self):
|
||||
"""propfind: ensure various NSURL work-arounds"""
|
||||
# a collection without /
|
||||
self._testPropfindURL('/SOGo/dav/%s' % username)
|
||||
# a collection with /
|
||||
self._testPropfindURL('/SOGo/dav/%s/' % username)
|
||||
# a non-collection
|
||||
self._testPropfindURL('/SOGo/dav/%s/freebusy.ifb' % username)
|
||||
|
||||
## REPORT
|
||||
def testPrincipalPropertySearch(self):
|
||||
"""principal-property-search"""
|
||||
resource = '/SOGo/dav'
|
||||
userInfo = self.dav_utility.fetchUserInfo(username)
|
||||
# subscriber_userInfo = self.dav_utility.fetchUserInfo(subscriber_username)
|
||||
matches = [["{urn:ietf:params:xml:ns:caldav}calendar-home-set",
|
||||
"/SOGo/dav/%s/Calendar" % username]]
|
||||
## the SOGo implementation does not support more than one
|
||||
## property-search at a time:
|
||||
# ["{urn:ietf:params:xml:ns:caldav}calendar-home-set",
|
||||
# "/SOGo/dav/%s/Calendar" % subscriber_username]]
|
||||
query = webdavlib.WebDAVPrincipalPropertySearch(resource,
|
||||
["displayname"], matches)
|
||||
self.client.execute(query)
|
||||
self.assertEquals(query.response["status"], 207)
|
||||
response = query.response["document"].findall('{DAV:}response')[0]
|
||||
href = response.find('{DAV:}href').text
|
||||
self.assertEquals("/SOGo/dav/%s/" % username, href)
|
||||
displayname = response.find('{DAV:}propstat/{DAV:}prop/{DAV:}displayname')
|
||||
value = displayname.text
|
||||
if value is None:
|
||||
value = ""
|
||||
self.assertEquals(userInfo[0], value)
|
||||
|
||||
# http://tools.ietf.org/html/rfc3253.html#section-3.8
|
||||
def testExpandProperty(self):
|
||||
"""expand-property"""
|
||||
resource = '/SOGo/dav/%s/' % username
|
||||
userInfo = self.dav_utility.fetchUserInfo(username)
|
||||
|
||||
query_props = {"{DAV:}owner": { "{DAV:}href": resource,
|
||||
"{DAV:}displayname": userInfo[0]},
|
||||
"{DAV:}principal-collection-set": { "{DAV:}href": "/SOGo/dav/",
|
||||
"{DAV:}displayname": "SOGo"}}
|
||||
query = webdavlib.WebDAVExpandProperty(resource, query_props.keys(),
|
||||
["displayname"])
|
||||
self.client.execute(query)
|
||||
self.assertEquals(query.response["status"], 207)
|
||||
|
||||
topResponse = query.response["document"].find('{DAV:}response')
|
||||
topHref = topResponse.find('{DAV:}href')
|
||||
self.assertEquals(resource, topHref.text)
|
||||
for query_prop in query_props.keys():
|
||||
propResponse = topResponse.find('{DAV:}propstat/{DAV:}prop/%s'
|
||||
% query_prop)
|
||||
propHref = propResponse.find('{DAV:}response/{DAV:}href')
|
||||
self.assertEquals(query_props[query_prop]["{DAV:}href"],
|
||||
propHref.text,
|
||||
"'%s', href mismatch: exp. '%s', got '%s'"
|
||||
% (query_prop,
|
||||
query_props[query_prop]["{DAV:}href"],
|
||||
propHref.text))
|
||||
propDisplayname = propResponse.find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}displayname')
|
||||
displayName = propDisplayname.text
|
||||
if displayName is None:
|
||||
displayName = ""
|
||||
self.assertEquals(query_props[query_prop]["{DAV:}displayname"],
|
||||
displayName,
|
||||
"'%s', displayname mismatch: exp. '%s', got '%s'"
|
||||
% (query_prop,
|
||||
query_props[query_prop]["{DAV:}displayname"],
|
||||
propDisplayname))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sogotests
|
||||
import unittest
|
||||
|
||||
from webdavlib import *
|
||||
|
||||
class HTTPUnparsedURLTest(unittest.TestCase):
|
||||
def testURLParse(self):
|
||||
fullURL = "http://username:password@hostname:123/folder/folder/object?param1=value1¶m2=value2"
|
||||
testURL = HTTPUnparsedURL(fullURL)
|
||||
self.assertEquals(testURL.protocol, "http")
|
||||
self.assertEquals(testURL.username, "username")
|
||||
self.assertEquals(testURL.password, "password")
|
||||
self.assertEquals(testURL.hostname, "hostname")
|
||||
self.assertEquals(testURL.port, "123")
|
||||
self.assertEquals(testURL.path, "/folder/folder/object")
|
||||
|
||||
exp_params = { "param1": "value1",
|
||||
"param2": "value2" }
|
||||
self.assertEquals(exp_params, testURL.parameters)
|
||||
|
||||
pathURL = "/folder/folder/simplereference"
|
||||
testURL = HTTPUnparsedURL(pathURL)
|
||||
self.assertEquals(testURL.protocol, None)
|
||||
self.assertEquals(testURL.username, None)
|
||||
self.assertEquals(testURL.password, None)
|
||||
self.assertEquals(testURL.hostname, None)
|
||||
self.assertEquals(testURL.port, None)
|
||||
self.assertEquals(testURL.path, "/folder/folder/simplereference")
|
||||
|
||||
pathURL = "http://user:secret@bla.com/hooray"
|
||||
testURL = HTTPUnparsedURL(pathURL)
|
||||
self.assertEquals(testURL.protocol, "http")
|
||||
self.assertEquals(testURL.username, "user")
|
||||
self.assertEquals(testURL.password, "secret")
|
||||
self.assertEquals(testURL.hostname, "bla.com")
|
||||
self.assertEquals(testURL.port, None)
|
||||
self.assertEquals(testURL.path, "/hooray")
|
||||
|
||||
pathURL = "http://user@bla.com:80/hooray"
|
||||
testURL = HTTPUnparsedURL(pathURL)
|
||||
self.assertEquals(testURL.protocol, "http")
|
||||
self.assertEquals(testURL.username, "user")
|
||||
self.assertEquals(testURL.password, None)
|
||||
self.assertEquals(testURL.hostname, "bla.com")
|
||||
self.assertEquals(testURL.port, "80")
|
||||
self.assertEquals(testURL.path, "/hooray")
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
from config import hostname, port, username, password
|
||||
|
||||
import math
|
||||
import sys
|
||||
import sogotests
|
||||
import time
|
||||
import unittest
|
||||
import webdavlib
|
||||
|
||||
resource = '/SOGo/dav/%s/Calendar/test-webdavsync/' % username
|
||||
|
||||
class WebdavSyncTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = webdavlib.WebDAVClient(hostname, port,
|
||||
username, password)
|
||||
|
||||
def tearDown(self):
|
||||
delete = webdavlib.WebDAVDELETE(resource)
|
||||
self.client.execute(delete)
|
||||
|
||||
def test(self):
|
||||
"""webdav sync"""
|
||||
# missing tests:
|
||||
# invalid tokens: negative, non-numeric, > current timestamp
|
||||
# non-empty collections: token validity, status codes for added,
|
||||
# modified and removed elements
|
||||
|
||||
# preparation
|
||||
mkcol = webdavlib.WebDAVMKCOL(resource)
|
||||
self.client.execute(mkcol)
|
||||
self.assertEquals(mkcol.response["status"], 201,
|
||||
"preparation: failure creating collection (code != 201)")
|
||||
|
||||
# test queries:
|
||||
# empty collection:
|
||||
# without a token (query1)
|
||||
# with a token (query2)
|
||||
# (when done, non-empty collection:
|
||||
# without a token (query3)
|
||||
# with a token (query4))
|
||||
|
||||
query1 = webdavlib.WebDAVSyncQuery(resource, None, [ "getetag" ])
|
||||
self.client.execute(query1)
|
||||
self.assertEquals(query1.response["status"], 207,
|
||||
("query1: invalid status code: %d (!= 207)"
|
||||
% query1.response["status"]))
|
||||
token_node = query1.response["document"].find("{DAV:}sync-token")
|
||||
# Implicit "assertion": we expect SOGo to return a token node, with a
|
||||
# non-empty numerical value. Anything else will trigger an exception
|
||||
token = int(token_node.text)
|
||||
|
||||
self.assertTrue(token >= 0)
|
||||
query1EndTime = int(math.ceil(query1.start + query1.duration))
|
||||
self.assertTrue(token <= query1EndTime,
|
||||
"token = %d > query1EndTime = %d" % (token, query1EndTime))
|
||||
|
||||
# we make sure that any token is accepted when the collection is
|
||||
# empty, but that the returned token differs
|
||||
query2 = webdavlib.WebDAVSyncQuery(resource, "1234", [ "getetag" ])
|
||||
self.client.execute(query2)
|
||||
self.assertEquals(query2.response["status"], 207)
|
||||
token_node = query2.response["document"].find("{DAV:}sync-token")
|
||||
self.assertTrue(token_node is not None,
|
||||
"expected 'sync-token' tag")
|
||||
token = int(token_node.text)
|
||||
self.assertTrue(token > 0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sogotests.runTests()
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import StringIO
|
||||
import sys
|
||||
import unittest
|
||||
import vobject
|
||||
import vobject.ics_diff
|
||||
import webdavlib
|
||||
import xml.sax.saxutils
|
||||
|
||||
class ics_compare():
|
||||
|
||||
def __init__(self, event1, event2):
|
||||
self.event1 = event1
|
||||
self.event2 = event2
|
||||
self.diffs = None
|
||||
|
||||
def _vcalendarComponent(self, event):
|
||||
event_component = None
|
||||
for item in vobject.readComponents(event):
|
||||
if item.name == "VCALENDAR":
|
||||
event_component = item
|
||||
return event_component
|
||||
|
||||
def areEqual(self):
|
||||
s_event1 = StringIO.StringIO(self.event1)
|
||||
s_event2 = StringIO.StringIO(self.event2)
|
||||
|
||||
event1_vcalendar = self._vcalendarComponent(s_event1)
|
||||
if event1_vcalendar is None:
|
||||
raise Exception("No VCALENDAR component in event1")
|
||||
|
||||
event2_vcalendar = self._vcalendarComponent(s_event2)
|
||||
if event2_vcalendar is None:
|
||||
raise Exception("No VCALENDAR component in event2")
|
||||
|
||||
self.diffs = vobject.ics_diff.diff(event1_vcalendar, event2_vcalendar)
|
||||
if not self.diffs:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def textDiff(self):
|
||||
saved_stdout = sys.stdout
|
||||
out = StringIO.StringIO()
|
||||
sys.stdout = out
|
||||
try :
|
||||
if self.diffs is not None:
|
||||
for (left, right) in self.diffs:
|
||||
left.prettyPrint()
|
||||
right.prettyPrint()
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
return out.getvalue().strip()
|
||||
|
||||
|
||||
class TestUtility():
|
||||
def __init__(self, test, client, resource = None):
|
||||
self.test = test
|
||||
self.client = client
|
||||
self.userInfo = {}
|
||||
|
||||
def fetchUserInfo(self, login):
|
||||
if not self.userInfo.has_key(login):
|
||||
resource = "/SOGo/dav/%s/" % login
|
||||
propfind = webdavlib.WebDAVPROPFIND(resource,
|
||||
["displayname",
|
||||
"{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"],
|
||||
0)
|
||||
self.client.execute(propfind)
|
||||
self.test.assertEquals(propfind.response["status"], 207)
|
||||
common_tree = "{DAV:}response/{DAV:}propstat/{DAV:}prop"
|
||||
name_nodes = propfind.response["document"] \
|
||||
.findall('%s/{DAV:}displayname' % common_tree)
|
||||
email_nodes = propfind.response["document"] \
|
||||
.findall('%s/{urn:ietf:params:xml:ns:caldav}calendar-user-address-set/{DAV:}href'
|
||||
% common_tree)
|
||||
|
||||
if len(name_nodes[0].text) > 0:
|
||||
displayName = name_nodes[0].text
|
||||
else:
|
||||
displayName = ""
|
||||
self.userInfo[login] = (displayName, email_nodes[0].text)
|
||||
|
||||
return self.userInfo[login]
|
||||
|
||||
class TestACLUtility(TestUtility):
|
||||
def __init__(self, test, client, resource):
|
||||
TestUtility.__init__(self, test, client, resource)
|
||||
self.resource = resource
|
||||
|
||||
def _subscriptionOperation(self, subscribers, operation):
|
||||
subscribeQuery = ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
+ "<%s" % operation
|
||||
+ " xmlns=\"urn:inverse:params:xml:ns:inverse-dav\"")
|
||||
if (subscribers is not None):
|
||||
subscribeQuery = (subscribeQuery
|
||||
+ " users=\"%s\"" % ",".join(subscribers))
|
||||
subscribeQuery = subscribeQuery + "/>"
|
||||
post = webdavlib.HTTPPOST(self.resource, subscribeQuery)
|
||||
post.content_type = "application/xml; charset=\"utf-8\""
|
||||
self.client.execute(post)
|
||||
self.test.assertEquals(post.response["status"], 200,
|
||||
"subscribtion failure to '%s' for '%s' (status: %d)"
|
||||
% (self.resource, "', '".join(subscribers),
|
||||
post.response["status"]))
|
||||
|
||||
def subscribe(self, subscribers=None):
|
||||
self._subscriptionOperation(subscribers, "subscribe")
|
||||
|
||||
def unsubscribe(self, subscribers=None):
|
||||
self._subscriptionOperation(subscribers, "unsubscribe")
|
||||
|
||||
def rightsToSOGoRights(self, rights):
|
||||
self.fail("subclass must implement this method")
|
||||
|
||||
def setupRights(self, username, rights = None):
|
||||
if rights is not None:
|
||||
rights_str = "".join(["<%s/>"
|
||||
% x for x in self.rightsToSOGoRights(rights) ])
|
||||
aclQuery = ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
+ "<acl-query"
|
||||
+ " xmlns=\"urn:inverse:params:xml:ns:inverse-dav\">"
|
||||
+ "<set-roles user=\"%s\">%s</set-roles>" % (xml.sax.saxutils.escape(username),
|
||||
rights_str)
|
||||
+ "</acl-query>")
|
||||
else:
|
||||
aclQuery = ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
+ "<acl-query"
|
||||
+ " xmlns=\"urn:inverse:params:xml:ns:inverse-dav\">"
|
||||
+ "<remove-user user=\"%s\"/>" % xml.sax.saxutils.escape(username)
|
||||
+ "</acl-query>")
|
||||
|
||||
post = webdavlib.HTTPPOST(self.resource, aclQuery)
|
||||
post.content_type = "application/xml; charset=\"utf-8\""
|
||||
self.client.execute(post)
|
||||
|
||||
if rights is None:
|
||||
err_msg = ("rights modification: failure to remove entry (status: %d)"
|
||||
% post.response["status"])
|
||||
else:
|
||||
err_msg = ("rights modification: failure to set '%s' (status: %d)"
|
||||
% (rights_str, post.response["status"]))
|
||||
self.test.assertEquals(post.response["status"], 204, err_msg)
|
||||
|
||||
# Calendar:
|
||||
# rights:
|
||||
# v: view all
|
||||
# d: view date and time
|
||||
# m: modify
|
||||
# r: respond
|
||||
# short rights notation: { "c": create,
|
||||
# "d": delete,
|
||||
# "pu": public,
|
||||
# "pr": private,
|
||||
# "co": confidential }
|
||||
class TestCalendarACLUtility(TestACLUtility):
|
||||
def rightsToSOGoRights(self, rights):
|
||||
sogoRights = []
|
||||
if rights.has_key("c") and rights["c"]:
|
||||
sogoRights.append("ObjectCreator")
|
||||
if rights.has_key("d") and rights["d"]:
|
||||
sogoRights.append("ObjectEraser")
|
||||
|
||||
classes = { "pu": "Public",
|
||||
"pr": "Private",
|
||||
"co": "Confidential" }
|
||||
rights_table = { "v": "Viewer",
|
||||
"d": "DAndTViewer",
|
||||
"m": "Modifier",
|
||||
"r": "Responder" }
|
||||
for k in classes.keys():
|
||||
if rights.has_key(k):
|
||||
right = rights[k]
|
||||
sogo_right = "%s%s" % (classes[k], rights_table[right])
|
||||
sogoRights.append(sogo_right)
|
||||
|
||||
return sogoRights
|
||||
|
||||
# Addressbook:
|
||||
# short rights notation: { "c": create,
|
||||
# "d": delete,
|
||||
# "e": edit,
|
||||
# "v": view }
|
||||
class TestAddressBookACLUtility(TestACLUtility):
|
||||
def rightsToSOGoRights(self, rights):
|
||||
sogoRightsTable = { "c": "ObjectCreator",
|
||||
"d": "ObjectEraser",
|
||||
"v": "ObjectViewer",
|
||||
"e": "ObjectEditor" }
|
||||
|
||||
sogoRights = []
|
||||
for k in rights.keys():
|
||||
sogoRights.append(sogoRightsTable[k])
|
||||
|
||||
return sogoRights
|
||||
|
||||
|
||||
@@ -1,581 +0,0 @@
|
||||
# webdavlib.py - A versatile WebDAV Python Library
|
||||
#
|
||||
# Copyright (C) 2009, 2010 Inverse inc.
|
||||
#
|
||||
# Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
#
|
||||
# webdavlib is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Lesser General Public License as published by the Free
|
||||
# Software Foundation; either version 2, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# webdavlib is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with webdavlib; see the file COPYING. If not, write to the Free
|
||||
# Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
||||
# USA.
|
||||
|
||||
import httplib
|
||||
import re
|
||||
import time
|
||||
import xml.dom.expatbuilder
|
||||
import xml.etree.cElementTree
|
||||
import xml.sax.saxutils
|
||||
import sys
|
||||
|
||||
xmlns_dav = "DAV:"
|
||||
xmlns_caldav = "urn:ietf:params:xml:ns:caldav"
|
||||
xmlns_carddav = "urn:ietf:params:xml:ns:carddav"
|
||||
xmlns_inversedav = "urn:inverse:params:xml:ns:inverse-dav"
|
||||
|
||||
url_re = None
|
||||
|
||||
class HTTPUnparsedURL:
|
||||
def __init__(self, url):
|
||||
self._parse(url)
|
||||
|
||||
def _parse(self, url):
|
||||
# ((proto)://((username(:(password)?)@)?hostname(:(port))))(path)?
|
||||
# if url_re is None:
|
||||
url_parts = url.split("?")
|
||||
alpha_match = "[a-zA-Z0-9%\._-]+"
|
||||
num_match = "[0-9]+"
|
||||
pattern = ("((%s)://(((%s)(:(%s))?@)?(%s)(:(%s))?))?(/.*)"
|
||||
% (alpha_match, alpha_match, alpha_match,
|
||||
alpha_match, num_match))
|
||||
url_re = re.compile(pattern)
|
||||
re_match = url_re.match(url_parts[0])
|
||||
if re_match is None:
|
||||
raise Exception, "URL expression could not be parsed: %s" % url
|
||||
|
||||
(trash, self.protocol, trash, trash, self.username, trash,
|
||||
self.password, self.hostname, trash, self.port, self.path) = re_match.groups()
|
||||
|
||||
self.parameters = {}
|
||||
if len(url_parts) > 1:
|
||||
param_elms = url_parts[1].split("&")
|
||||
for param_pair in param_elms:
|
||||
parameter = param_pair.split("=")
|
||||
self.parameters[parameter[0]] = parameter[1]
|
||||
|
||||
class WebDAVClient:
|
||||
user_agent = "Mozilla/5.0"
|
||||
|
||||
def __init__(self, hostname, port, username = None, password = "",
|
||||
forcessl = False):
|
||||
if int(port) == 443 or forcessl:
|
||||
import M2Crypto.httpslib
|
||||
self.conn = M2Crypto.httpslib.HTTPSConnection(hostname, int(port),
|
||||
True)
|
||||
else:
|
||||
self.conn = httplib.HTTPConnection(hostname, port, True)
|
||||
|
||||
if username is None:
|
||||
self.simpleauth_hash = None
|
||||
else:
|
||||
self.simpleauth_hash = (("%s:%s" % (username, password))
|
||||
.encode('base64')[:-1])
|
||||
|
||||
def prepare_headers(self, query, body):
|
||||
headers = { "User-Agent": self.user_agent }
|
||||
if self.simpleauth_hash is not None:
|
||||
headers["authorization"] = "Basic %s" % self.simpleauth_hash
|
||||
if body is not None:
|
||||
headers["content-length"] = len(body)
|
||||
if query.__dict__.has_key("depth") and query.depth is not None:
|
||||
headers["depth"] = query.depth
|
||||
if query.__dict__.has_key("content_type"):
|
||||
headers["content-type"] = query.content_type
|
||||
if not query.__dict__.has_key("accept-language"):
|
||||
headers["accept-language"] = 'en-us,en;q=0.5'
|
||||
|
||||
query_headers = query.prepare_headers()
|
||||
if query_headers is not None:
|
||||
for key in query_headers.keys():
|
||||
headers[key] = query_headers[key]
|
||||
|
||||
return headers
|
||||
|
||||
def execute(self, query):
|
||||
body = query.render()
|
||||
|
||||
query.start = time.time()
|
||||
self.conn.request(query.method, query.url,
|
||||
body, self.prepare_headers(query, body))
|
||||
try:
|
||||
query.set_response(self.conn.getresponse());
|
||||
except httplib.BadStatusLine, e:
|
||||
print e
|
||||
time.sleep(3)
|
||||
query.set_response(self.conn.getresponse());
|
||||
query.duration = time.time() - query.start
|
||||
|
||||
class HTTPSimpleQuery:
|
||||
method = None
|
||||
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
self.response = None
|
||||
self.start = -1
|
||||
self.duration = -1
|
||||
|
||||
def prepare_headers(self):
|
||||
return {}
|
||||
|
||||
def render(self):
|
||||
return None
|
||||
|
||||
def set_response(self, http_response):
|
||||
headers = {}
|
||||
for rk, rv in http_response.getheaders():
|
||||
k = rk.lower()
|
||||
headers[k] = rv
|
||||
self.response = { "headers": headers,
|
||||
"status": http_response.status,
|
||||
"version": http_response.version,
|
||||
"body": http_response.read() }
|
||||
|
||||
class HTTPGET(HTTPSimpleQuery):
|
||||
method = "GET"
|
||||
cookie = None
|
||||
|
||||
def prepare_headers (self):
|
||||
headers = HTTPSimpleQuery.prepare_headers(self)
|
||||
if self.cookie:
|
||||
headers["Cookie"] = self.cookie
|
||||
return headers
|
||||
|
||||
|
||||
class HTTPOPTIONS(HTTPSimpleQuery):
|
||||
method = "OPTIONS"
|
||||
|
||||
class HTTPQuery(HTTPSimpleQuery):
|
||||
def __init__(self, url):
|
||||
HTTPSimpleQuery.__init__(self, url)
|
||||
self.content_type = "application/octet-stream"
|
||||
|
||||
class HTTPPUT(HTTPQuery):
|
||||
method = "PUT"
|
||||
|
||||
def __init__(self, url, content,
|
||||
content_type="application/octet-stream",
|
||||
exclusive=False):
|
||||
HTTPQuery.__init__(self, url)
|
||||
self.content = content
|
||||
self.content_type = content_type
|
||||
self.exclusive = exclusive
|
||||
|
||||
def render(self):
|
||||
return self.content
|
||||
|
||||
def prepare_headers(self):
|
||||
headers = HTTPQuery.prepare_headers(self)
|
||||
if self.exclusive:
|
||||
headers["if-none-match"] = "*"
|
||||
|
||||
return headers
|
||||
|
||||
class HTTPPOST(HTTPPUT):
|
||||
method = "POST"
|
||||
cookie = None
|
||||
|
||||
def prepare_headers (self):
|
||||
headers = HTTPPUT.prepare_headers(self)
|
||||
if self.cookie:
|
||||
headers["Cookie"] = self.cookie
|
||||
return headers
|
||||
|
||||
|
||||
|
||||
class WebDAVQuery(HTTPQuery):
|
||||
method = None
|
||||
|
||||
def __init__(self, url, depth = None):
|
||||
HTTPQuery.__init__(self, url)
|
||||
self.content_type = "application/xml; charset=\"utf-8\""
|
||||
self.depth = depth
|
||||
self.ns_mgr = _WD_XMLNS_MGR()
|
||||
self.top_node = None
|
||||
|
||||
# helper for PROPFIND and REPORT (only)
|
||||
def _initProperties(self, properties):
|
||||
props = _WD_XMLTreeElement("prop")
|
||||
self.top_node.append(props)
|
||||
for prop in properties:
|
||||
prop_tag = self.render_tag(prop)
|
||||
props.append(_WD_XMLTreeElement(prop_tag))
|
||||
|
||||
def render(self):
|
||||
if self.top_node is not None:
|
||||
text = ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n%s"
|
||||
% self.top_node.render(self.ns_mgr.render()))
|
||||
else:
|
||||
text = ""
|
||||
|
||||
return text
|
||||
|
||||
def render_tag(self, tag):
|
||||
cb = tag.find("}")
|
||||
if cb > -1:
|
||||
ns = tag[1:cb]
|
||||
real_tag = tag[cb+1:]
|
||||
new_tag = self.ns_mgr.register(real_tag, ns)
|
||||
else:
|
||||
new_tag = tag
|
||||
|
||||
return new_tag
|
||||
|
||||
def set_response(self, http_response):
|
||||
HTTPQuery.set_response(self, http_response)
|
||||
headers = self.response["headers"]
|
||||
if (headers.has_key("content-type")
|
||||
and headers.has_key("content-length")
|
||||
and (headers["content-type"].startswith("application/xml")
|
||||
or headers["content-type"].startswith("text/xml"))
|
||||
and int(headers["content-length"]) > 0):
|
||||
document = xml.etree.cElementTree.fromstring(self.response["body"])
|
||||
self.response["document"] = document
|
||||
|
||||
class WebDAVMKCOL(WebDAVQuery):
|
||||
method = "MKCOL"
|
||||
|
||||
class WebDAVDELETE(WebDAVQuery):
|
||||
method = "DELETE"
|
||||
|
||||
class WebDAVREPORT(WebDAVQuery):
|
||||
method = "REPORT"
|
||||
|
||||
class WebDAVGET(WebDAVQuery):
|
||||
method = "GET"
|
||||
|
||||
class WebDAVPROPFIND(WebDAVQuery):
|
||||
method = "PROPFIND"
|
||||
|
||||
def __init__(self, url, properties, depth = None):
|
||||
WebDAVQuery.__init__(self, url, depth)
|
||||
self.top_node = _WD_XMLTreeElement("propfind")
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
class WebDAVPROPPATCH(WebDAVQuery):
|
||||
method = "PROPPATCH"
|
||||
|
||||
# <x0:propertyupdate xmlns:x1="urn:ietf:params:xml:ns:caldav" xmlns:x0="DAV:"><x0:set><x0:prop>
|
||||
|
||||
def __init__(self, url, properties):
|
||||
WebDAVQuery.__init__(self, url, None)
|
||||
self.top_node = _WD_XMLTreeElement("propertyupdate")
|
||||
set_node = _WD_XMLTreeElement("set")
|
||||
self.top_node.append(set_node)
|
||||
prop_node = _WD_XMLTreeElement("prop")
|
||||
set_node.append(prop_node)
|
||||
|
||||
prop_node.appendSubtree(self, properties)
|
||||
|
||||
class WebDAVMOVE(WebDAVQuery):
|
||||
method = "MOVE"
|
||||
destination = None
|
||||
host = None
|
||||
|
||||
def prepare_headers(self):
|
||||
headers = WebDAVQuery.prepare_headers(self)
|
||||
print "DESTINATION", self.destination
|
||||
if self.destination is not None:
|
||||
headers["Destination"] = self.destination
|
||||
if self.host is not None:
|
||||
headers["Host"] = self.host
|
||||
return headers
|
||||
|
||||
class WebDAVPrincipalPropertySearch(WebDAVREPORT):
|
||||
def __init__(self, url, properties, matches):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
ppsearch_tag = self.ns_mgr.register("principal-property-search",
|
||||
xmlns_dav)
|
||||
self.top_node = _WD_XMLTreeElement(ppsearch_tag)
|
||||
self._initMatches(matches)
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
def _initMatches(self, matches):
|
||||
for match in matches:
|
||||
psearch = _WD_XMLTreeElement("property-search")
|
||||
self.top_node.append(psearch)
|
||||
prop = _WD_XMLTreeElement("prop")
|
||||
psearch.append(prop)
|
||||
match_tag = self.render_tag(match[0])
|
||||
prop.append(_WD_XMLTreeElement(match_tag))
|
||||
match_tag = _WD_XMLTreeElement("match")
|
||||
psearch.append(match_tag)
|
||||
match_tag.appendSubtree(self, match[1])
|
||||
|
||||
class WebDAVSyncQuery(WebDAVREPORT):
|
||||
def __init__(self, url, token, properties):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
self.top_node = _WD_XMLTreeElement("sync-collection")
|
||||
|
||||
sync_token = _WD_XMLTreeElement("sync-token")
|
||||
self.top_node.append(sync_token)
|
||||
if token is not None:
|
||||
sync_token.append(_WD_XMLTreeTextNode(token))
|
||||
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
class WebDAVExpandProperty(WebDAVREPORT):
|
||||
def _parseTag(self, tag):
|
||||
result = []
|
||||
|
||||
cb = tag.find("}")
|
||||
if cb > -1:
|
||||
result.append(tag[cb+1:])
|
||||
result.append(tag[1:cb])
|
||||
else:
|
||||
result.append(tag)
|
||||
result.append("DAV:")
|
||||
|
||||
return result;
|
||||
|
||||
def _propElement(self, tag):
|
||||
parsedTag = self._parseTag(tag)
|
||||
parameters = { "name": parsedTag[0] }
|
||||
if len(parsedTag) > 1:
|
||||
parameters["namespace"] = parsedTag[1]
|
||||
|
||||
return _WD_XMLTreeElement("property", parameters)
|
||||
|
||||
def __init__(self, url, query_properties, properties):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
self.top_node = _WD_XMLTreeElement("expand-property")
|
||||
|
||||
for query_tag in query_properties:
|
||||
property_query = self._propElement(query_tag)
|
||||
self.top_node.append(property_query)
|
||||
for tag in properties:
|
||||
property = self._propElement(tag)
|
||||
property_query.append(property)
|
||||
|
||||
class CalDAVPOST(WebDAVQuery):
|
||||
method = "POST"
|
||||
|
||||
def __init__(self, url, content,
|
||||
originator = None, recipients = None):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
self.content_type = "text/calendar; charset=utf-8"
|
||||
self.originator = originator
|
||||
self.recipients = recipients
|
||||
self.content = content
|
||||
|
||||
def prepare_headers(self):
|
||||
headers = WebDAVQuery.prepare_headers(self)
|
||||
|
||||
if self.originator is not None:
|
||||
headers["originator"] = self.originator
|
||||
|
||||
if self.recipients is not None:
|
||||
headers["recipient"] = ",".join(self.recipients)
|
||||
|
||||
return headers
|
||||
|
||||
def render(self):
|
||||
return self.content
|
||||
|
||||
class CalDAVCalendarMultiget(WebDAVREPORT):
|
||||
def __init__(self, url, properties, hrefs, depth = None):
|
||||
WebDAVQuery.__init__(self, url, depth)
|
||||
multiget_tag = self.ns_mgr.register("calendar-multiget", xmlns_caldav)
|
||||
self.top_node = _WD_XMLTreeElement(multiget_tag)
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
for href in hrefs:
|
||||
href_node = _WD_XMLTreeElement("href")
|
||||
self.top_node.append(href_node)
|
||||
href_node.append(_WD_XMLTreeTextNode(href))
|
||||
|
||||
class CalDAVCalendarQuery(WebDAVREPORT):
|
||||
def __init__(self, url, properties, component = None, timerange = None):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
multiget_tag = self.ns_mgr.register("calendar-query", xmlns_caldav)
|
||||
self.top_node = _WD_XMLTreeElement(multiget_tag)
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
if component is not None:
|
||||
filter_tag = self.ns_mgr.register("filter",
|
||||
xmlns_caldav)
|
||||
compfilter_tag = self.ns_mgr.register("comp-filter",
|
||||
xmlns_caldav)
|
||||
filter_node = _WD_XMLTreeElement(filter_tag)
|
||||
cal_filter_node = _WD_XMLTreeElement(compfilter_tag,
|
||||
{ "name": "VCALENDAR" })
|
||||
comp_node = _WD_XMLTreeElement(compfilter_tag,
|
||||
{ "name": component })
|
||||
## TODO
|
||||
# if timerange is not None:
|
||||
cal_filter_node.append(comp_node)
|
||||
filter_node.append(cal_filter_node)
|
||||
self.top_node.append(filter_node)
|
||||
|
||||
class CardDAVAddressBookQuery(WebDAVREPORT):
|
||||
def __init__(self, url, properties, searchProperty = None, searchValue = None):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
query_tag = self.ns_mgr.register("addressbook-query", xmlns_carddav)
|
||||
ns_key = self.ns_mgr.xmlns[xmlns_carddav]
|
||||
self.top_node = _WD_XMLTreeElement(query_tag)
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
if searchProperty is not None:
|
||||
filter_node = _WD_XMLTreeElement("%s:filter" % ns_key)
|
||||
self.top_node.append(filter_node)
|
||||
propfilter_node = _WD_XMLTreeElement("%s:prop-filter" % ns_key, { "name": searchProperty })
|
||||
filter_node.append(propfilter_node)
|
||||
match_node = _WD_XMLTreeElement("%s:text-match" % ns_key,
|
||||
{ "collation": "i;unicasemap", "match-type": "starts-with" })
|
||||
propfilter_node.append(match_node)
|
||||
match_node.appendSubtree(None, searchValue)
|
||||
|
||||
class MailDAVMailQuery(WebDAVREPORT):
|
||||
def __init__(self, url, properties, filters = None,
|
||||
sort = None, ascending = True):
|
||||
WebDAVQuery.__init__(self, url)
|
||||
mailquery_tag = self.ns_mgr.register("mail-query",
|
||||
xmlns_inversedav)
|
||||
self.top_node = _WD_XMLTreeElement(mailquery_tag)
|
||||
if properties is not None and len(properties) > 0:
|
||||
self._initProperties(properties)
|
||||
|
||||
if filters is not None and len(filters) > 0:
|
||||
self._initFilters(filters)
|
||||
|
||||
if sort is not None and len(sort) > 0:
|
||||
self._initSort(sort, ascending)
|
||||
|
||||
def _initFilters(self, filters):
|
||||
mailfilter_tag = self.ns_mgr.register("mail-filters",
|
||||
xmlns_inversedav)
|
||||
mailfilter_node = _WD_XMLTreeElement(mailfilter_tag)
|
||||
self.top_node.append(mailfilter_node)
|
||||
for filterk in filters.keys():
|
||||
filter_tag = self.ns_mgr.register(filterk,
|
||||
xmlns_inversedav)
|
||||
filter_node = _WD_XMLTreeElement(filter_tag,
|
||||
filters[filterk])
|
||||
mailfilter_node.append(filter_node)
|
||||
|
||||
def _initSort(self, sort, ascending):
|
||||
sort_tag = self.ns_mgr.register("sort", xmlns_inversedav)
|
||||
if ascending:
|
||||
sort_attrs = { "order": "ascending" }
|
||||
else:
|
||||
sort_attrs = { "order": "descending" }
|
||||
sort_node = _WD_XMLTreeElement(sort_tag, sort_attrs)
|
||||
self.top_node.append(sort_node)
|
||||
|
||||
for item in sort:
|
||||
sort_subnode = _WD_XMLTreeElement(self.render_tag(item))
|
||||
sort_node.append(sort_subnode)
|
||||
|
||||
# private classes to handle XML stuff
|
||||
class _WD_XMLNS_MGR:
|
||||
def __init__(self):
|
||||
self.xmlns = {}
|
||||
self.counter = 0
|
||||
|
||||
def render(self):
|
||||
text = " xmlns=\"DAV:\""
|
||||
for k in self.xmlns:
|
||||
text = text + " xmlns:%s=\"%s\"" % (self.xmlns[k], k)
|
||||
|
||||
return text
|
||||
|
||||
def create_key(self, namespace):
|
||||
new_nssym = "n%d" % self.counter
|
||||
self.counter = self.counter + 1
|
||||
self.xmlns[namespace] = new_nssym
|
||||
|
||||
return new_nssym
|
||||
|
||||
def register(self, tag, namespace):
|
||||
if namespace != xmlns_dav:
|
||||
if self.xmlns.has_key(namespace):
|
||||
key = self.xmlns[namespace]
|
||||
else:
|
||||
key = self.create_key(namespace)
|
||||
else:
|
||||
key = None
|
||||
|
||||
if key is not None:
|
||||
newTag = "%s:%s" % (key, tag)
|
||||
else:
|
||||
newTag = tag
|
||||
|
||||
return newTag
|
||||
|
||||
class _WD_XMLTreeElement:
|
||||
typeNum = type(0)
|
||||
typeStr = type("")
|
||||
typeUnicode = type(u"")
|
||||
typeList = type([])
|
||||
typeDict = type({})
|
||||
|
||||
def __init__(self, tag, attributes = {}):
|
||||
self.tag = tag
|
||||
self.children = []
|
||||
self.attributes = attributes
|
||||
|
||||
def append(self, child):
|
||||
self.children.append(child)
|
||||
|
||||
def appendSubtree(self, query, subtree):
|
||||
if type(subtree) == self.typeNum:
|
||||
strValue = "%d" % subtree
|
||||
textNode = _WD_XMLTreeTextNode(strValue)
|
||||
self.append(textNode)
|
||||
elif type(subtree) == self.typeUnicode:
|
||||
textNode = _WD_XMLTreeTextNode(subtree.encode("utf-8"))
|
||||
self.append(textNode)
|
||||
elif type(subtree) == self.typeStr:
|
||||
textNode = _WD_XMLTreeTextNode(subtree)
|
||||
self.append(textNode)
|
||||
elif type(subtree) == self.typeList:
|
||||
for x in subtree:
|
||||
self.appendSubtree(query, x)
|
||||
elif type(subtree) == self.typeDict:
|
||||
for x in subtree.keys():
|
||||
tag = query.render_tag(x)
|
||||
node = _WD_XMLTreeElement(tag)
|
||||
node.appendSubtree(query, subtree[x])
|
||||
self.append(node)
|
||||
else:
|
||||
None
|
||||
|
||||
def render(self, ns_text = None):
|
||||
text = "<" + self.tag
|
||||
|
||||
if ns_text is not None:
|
||||
text = text + ns_text
|
||||
|
||||
for k in self.attributes:
|
||||
text = text + " %s=\"%s\"" % (k, self.attributes[k])
|
||||
|
||||
if len(self.children) > 0:
|
||||
text = text + ">"
|
||||
for child in self.children:
|
||||
text = text + child.render()
|
||||
text = text + "</" + self.tag + ">"
|
||||
else:
|
||||
text = text + "/>"
|
||||
|
||||
return text
|
||||
|
||||
class _WD_XMLTreeTextNode:
|
||||
def __init__(self, text):
|
||||
self.text = xml.sax.saxutils.escape(text)
|
||||
|
||||
def render(self):
|
||||
return self.text
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import getopt
|
||||
import sys
|
||||
import urlparse
|
||||
import webdavlib
|
||||
import xml.dom.minidom
|
||||
|
||||
def usage() :
|
||||
msg ="""Usage:
|
||||
%s [-h] [-s sync-token] -u uri\n""" % sys.argv[0]
|
||||
|
||||
sys.stderr.write(msg);
|
||||
|
||||
def getAllCollections(client, uri):
|
||||
collections = []
|
||||
depth = 1
|
||||
|
||||
propfind = webdavlib.WebDAVPROPFIND(uri, ["allprop"], depth)
|
||||
client.execute(propfind)
|
||||
client.conn.close()
|
||||
doc = propfind.response["document"]
|
||||
for response in doc.iter("{DAV:}response"):
|
||||
propstat = response.find("{DAV:}propstat")
|
||||
if propstat is not None:
|
||||
prop = propstat.find("{DAV:}prop")
|
||||
if prop is not None:
|
||||
resourcetype = prop.find("{DAV:}resourcetype")
|
||||
if resourcetype.find("{DAV:}collection") is not None:
|
||||
href = prop.find("{DAV:}href")
|
||||
if href is not None and href.text != uri:
|
||||
collections.append(href.text)
|
||||
return collections
|
||||
|
||||
def changedItemsFromCollection(client, collection, synctoken=None):
|
||||
# get all changed hrefs since synctoken
|
||||
hrefs = []
|
||||
syncquery = webdavlib.WebDAVSyncQuery(collection, synctoken, [ "getcontenttype", "getetag" ])
|
||||
client.execute(syncquery)
|
||||
client.conn.close()
|
||||
if (syncquery.response["status"] != 207):
|
||||
raise Exception("Bad http response code: %d" % syncquery.response["status"])
|
||||
doc = syncquery.response["document"]
|
||||
|
||||
# extract all hrefs
|
||||
for syncResponse in doc.iter("{DAV:}response"):
|
||||
href = syncResponse.find("{DAV:}href")
|
||||
if href is not None:
|
||||
hrefs.append(href.text)
|
||||
|
||||
return hrefs
|
||||
|
||||
|
||||
def main():
|
||||
depth = 1
|
||||
synctoken = "1"
|
||||
url = None
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt (sys.argv[1:], "hs:u:", \
|
||||
("sync-token=", "url="));
|
||||
except getopt.GetoptError:
|
||||
usage()
|
||||
exit(1)
|
||||
|
||||
for o, v in opts :
|
||||
if o == "-h" :
|
||||
usage()
|
||||
exit(1)
|
||||
elif o == "-s" or o == "--sync-token" :
|
||||
synctoken = v
|
||||
elif o == "-u" or o == "--url" :
|
||||
url = v
|
||||
|
||||
if url is None:
|
||||
usage()
|
||||
exit(1)
|
||||
|
||||
o = urlparse.urlparse(url)
|
||||
hostname = o.hostname
|
||||
port = o.port
|
||||
username = o.username
|
||||
password = o.password
|
||||
uri = o.path
|
||||
|
||||
client = webdavlib.WebDAVClient(hostname, port, username, password)
|
||||
|
||||
collections = getAllCollections(client, uri)
|
||||
if len(collections) == 0:
|
||||
print "No collections found!"
|
||||
sys.exit(1)
|
||||
|
||||
for collection in collections:
|
||||
changedItems = changedItemsFromCollection(client, collection)
|
||||
# fetch the href data
|
||||
if len(changedItems) > 0:
|
||||
multiget = webdavlib.CalDAVCalendarMultiget(collection,
|
||||
["getetag", "{%s}calendar-data" % webdavlib.xmlns_caldav],
|
||||
changedItems, depth)
|
||||
client.execute(multiget)
|
||||
client.conn.close()
|
||||
if (multiget.response["status"] != 207):
|
||||
raise Exception("Bad http response code: %d" % multiget.response["status"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
16
Tests/README.md
Normal file
16
Tests/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Tests
|
||||
|
||||
This directory holds automated tests for SOGo.
|
||||
|
||||
- `spec` and `lib`: hold JavaScript driven interated tests that are used to validate overall DAV functionality
|
||||
- `Unit`: holds all unit tests
|
||||
|
||||
The configuration is found in `lib/config.js`.
|
||||
|
||||
## Tools
|
||||
|
||||
* [Jasmin](https://jasmine.github.io/) - testing framework
|
||||
* [tsdav](https://tsdav.vercel.app/) - webdav request helper
|
||||
* [ical.js](https://github.com/mozilla-comm/ical.js) - ics and vcard parser
|
||||
* [cross-fetch](https://github.com/lquixada/cross-fetch) - fetch API
|
||||
* [xml-js](https://github.com/nashwaan/xml-js) - convert JS object to XML
|
||||
557
Tests/lib/WebDAV.js
Normal file
557
Tests/lib/WebDAV.js
Normal file
@@ -0,0 +1,557 @@
|
||||
import cookie from 'cookie'
|
||||
import {
|
||||
DAVNamespace,
|
||||
DAVNamespaceShorthandMap,
|
||||
|
||||
davRequest,
|
||||
deleteObject,
|
||||
formatProps,
|
||||
getBasicAuthHeaders,
|
||||
getDAVAttribute,
|
||||
propfind,
|
||||
|
||||
calendarMultiGet,
|
||||
createCalendarObject,
|
||||
makeCalendar,
|
||||
|
||||
createVCard
|
||||
} from 'tsdav'
|
||||
import convert from 'xml-js'
|
||||
import { fetch } from 'cross-fetch'
|
||||
import config from './config'
|
||||
|
||||
const DAVInverse = 'urn:inverse:params:xml:ns:inverse-dav'
|
||||
const DAVInverseShort = 'i'
|
||||
const DAVMailHeader = 'urn:schemas:mailheader:'
|
||||
const DAVMailHeaderShort = 'mh'
|
||||
const DAVHttpMail = 'urn:schemas:httpmail:'
|
||||
const DAVHttpMailShort = 'hm'
|
||||
const DAVnsShortMap = {
|
||||
[DAVInverse]: DAVInverseShort,
|
||||
[DAVMailHeader]: DAVMailHeaderShort,
|
||||
[DAVHttpMail]: DAVHttpMailShort,
|
||||
...DAVNamespaceShorthandMap
|
||||
}
|
||||
|
||||
export {
|
||||
DAVInverse, DAVInverseShort,
|
||||
DAVMailHeader, DAVMailHeaderShort,
|
||||
DAVHttpMail, DAVHttpMailShort
|
||||
}
|
||||
|
||||
class WebDAV {
|
||||
constructor(un, pw) {
|
||||
this.serverUrl = `http://${config.hostname}:${config.port}`
|
||||
this.cookie = null
|
||||
if (un && pw) {
|
||||
this.username = un
|
||||
this.password = pw
|
||||
this.headers = getBasicAuthHeaders({
|
||||
username: un,
|
||||
password: pw
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.headers = {}
|
||||
}
|
||||
}
|
||||
|
||||
// Generic operations
|
||||
|
||||
async getAuthCookie() {
|
||||
if (!this.cookie) {
|
||||
const resource = `/SOGo/connect`
|
||||
const response = await fetch(this.serverUrl + resource, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({userName: this.username, password: this.password})
|
||||
})
|
||||
const values = response.headers.get('set-cookie').split(/, /)
|
||||
let authCookies = []
|
||||
for (let v of values) {
|
||||
let c = cookie.parse(v)
|
||||
for (let authCookie of ['0xHIGHFLYxSOGo', 'XSRF-TOKEN']) {
|
||||
if (Object.keys(c).includes('0xHIGHFLYxSOGo')) {
|
||||
authCookies.push(cookie.serialize(authCookie, c[authCookie]))
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cookie = authCookies.join('; ')
|
||||
}
|
||||
return this.cookie
|
||||
}
|
||||
|
||||
async getHttp(resource) {
|
||||
const authCookie = await this.getAuthCookie()
|
||||
const localHeaders = { Cookie: authCookie }
|
||||
|
||||
return await fetch(this.serverUrl + resource, {
|
||||
method: 'GET',
|
||||
headers: localHeaders
|
||||
})
|
||||
}
|
||||
|
||||
async postHttp(resource, contentType = 'application/json', data = '') {
|
||||
const authCookie = await this.getAuthCookie()
|
||||
const localHeaders = { 'Content-Type': contentType, Cookie: authCookie }
|
||||
|
||||
return await fetch(this.serverUrl + resource, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
headers: localHeaders
|
||||
})
|
||||
}
|
||||
|
||||
// WebDAV operations
|
||||
|
||||
deleteObject(resource) {
|
||||
return deleteObject({
|
||||
url: this.serverUrl + resource,
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
getObject(resource, filename) {
|
||||
let url
|
||||
if (resource.match(/^http/))
|
||||
url = resource
|
||||
else
|
||||
url = this.serverUrl + resource
|
||||
if (filename)
|
||||
url += filename
|
||||
return davRequest({
|
||||
url,
|
||||
init: {
|
||||
method: 'GET',
|
||||
headers: this.headers,
|
||||
body: null
|
||||
},
|
||||
convertIncoming: false
|
||||
})
|
||||
}
|
||||
|
||||
makeCollection(resource) {
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'MKCOL',
|
||||
headers: this.headers,
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
propfindWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) {
|
||||
const nsShort = DAVnsShortMap[namespace] || DAVInverseShort
|
||||
const formattedProperties = properties.map(p => {
|
||||
return { [`${nsShort}:${p}`]: '' }
|
||||
})
|
||||
let url
|
||||
if (resource.match(/^http/))
|
||||
url = resource
|
||||
else
|
||||
url = this.serverUrl + resource
|
||||
if (typeof headers.depth == 'undefined') {
|
||||
headers.depth = new String(0)
|
||||
}
|
||||
return davRequest({
|
||||
url,
|
||||
init: {
|
||||
method: 'PROPFIND',
|
||||
headers: { ...this.headers, ...headers },
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
body: {
|
||||
propfind: {
|
||||
_attributes: {
|
||||
...getDAVAttribute([
|
||||
DAVNamespace.CALDAV,
|
||||
DAVNamespace.CALDAV_APPLE,
|
||||
DAVNamespace.CALENDAR_SERVER,
|
||||
DAVNamespace.CARDDAV,
|
||||
DAVNamespace.DAV
|
||||
]),
|
||||
[`xmlns:${nsShort}`]: namespace
|
||||
},
|
||||
prop: formattedProperties
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
propfindWebdavRaw(resource, properties, headers = {}) {
|
||||
const namespace = DAVNamespaceShorthandMap[DAVNamespace.DAV]
|
||||
const formattedProperties = properties.map(prop => {
|
||||
return { [`${namespace}:${prop}`]: '' }
|
||||
})
|
||||
|
||||
let xmlBody = convert.js2xml(
|
||||
{
|
||||
propfind: {
|
||||
_attributes: getDAVAttribute([DAVNamespace.DAV]),
|
||||
prop: formattedProperties
|
||||
}
|
||||
},
|
||||
{
|
||||
compact: true,
|
||||
spaces: 2,
|
||||
elementNameFn: (name) => {
|
||||
// add namespace to all keys without namespace
|
||||
if (!/^.+:.+/.test(name)) {
|
||||
return `${namespace}:${name}`;
|
||||
}
|
||||
return name;
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return fetch(this.serverUrl + resource, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml; charset="utf-8"',
|
||||
...headers,
|
||||
...this.headers
|
||||
},
|
||||
method: 'PROPFIND',
|
||||
body: xmlBody
|
||||
})
|
||||
}
|
||||
|
||||
propfindURL(resource = '/SOGo/dav') {
|
||||
return propfind({
|
||||
url: this.serverUrl + resource,
|
||||
depth: '1',
|
||||
props: [
|
||||
{ name: 'displayname', namespace: DAVNamespace.DAV },
|
||||
{ name: 'resourcetype', namespace: DAVNamespace.DAV }
|
||||
],
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
propfindCollection(resource) {
|
||||
return propfind({
|
||||
url: this.serverUrl + resource,
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
// http://tools.ietf.org/html/rfc3253.html#section-3.8
|
||||
expendProperty(resource, properties) {
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'REPORT',
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
headers: this.headers,
|
||||
body: {
|
||||
'expand-property': {
|
||||
_attributes: getDAVAttribute([
|
||||
DAVNamespace.DAV,
|
||||
]),
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:property`]: properties
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
proppatchWebdav(resource, properties, namespace = DAVNamespace.DAV, headers = {}) {
|
||||
const nsShort = DAVNamespaceShorthandMap[namespace] || DAVInverseShort
|
||||
const formattedProperties = Object.keys(properties).map(p => {
|
||||
if (Array.isArray(properties[p])) {
|
||||
return { [`${nsShort}:${p}`]: properties[p].map(pp => {
|
||||
const [ key ] = Object.keys(pp)
|
||||
return { [`${nsShort}:${key}`]: pp[key] || '' }
|
||||
})}
|
||||
}
|
||||
return { [`${nsShort}:${p}`]: properties[p] || '' }
|
||||
})
|
||||
if (typeof headers.depth == 'undefined') {
|
||||
headers.depth = new String(0)
|
||||
}
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'PROPPATCH',
|
||||
headers: { ...this.headers, ...headers },
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
body: {
|
||||
propertyupdate: {
|
||||
_attributes: {
|
||||
...getDAVAttribute([
|
||||
DAVNamespace.CALDAV,
|
||||
DAVNamespace.CALDAV_APPLE,
|
||||
DAVNamespace.CALENDAR_SERVER,
|
||||
DAVNamespace.CARDDAV,
|
||||
DAVNamespace.DAV
|
||||
]),
|
||||
[`xmlns:${nsShort}`]: namespace
|
||||
},
|
||||
set: {
|
||||
prop: formattedProperties
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
currentUserPrivilegeSet(resource) {
|
||||
return propfind({
|
||||
url: this.serverUrl + resource,
|
||||
depth: '0',
|
||||
props: [
|
||||
{ name: 'current-user-privilege-set', namespace: DAVNamespace.DAV }
|
||||
],
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
options(resource) {
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'OPTIONS',
|
||||
headers: this.headers,
|
||||
body: null
|
||||
},
|
||||
convertIncoming: false
|
||||
})
|
||||
}
|
||||
|
||||
principalCollectionSet(resource = '/SOGo/dav') {
|
||||
return propfind({
|
||||
url: this.serverUrl + resource,
|
||||
depth: '0',
|
||||
props: [{ name: 'principal-collection-set', namespace: DAVNamespace.DAV }],
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6578#section-3.2
|
||||
syncCollectionRaw(resource, token = '', properties) {
|
||||
const formattedProperties = properties.map((p) => {
|
||||
return { [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${p}`]: '' }
|
||||
});
|
||||
let xmlBody = convert.js2xml(
|
||||
{
|
||||
'sync-collection': {
|
||||
_attributes: getDAVAttribute([DAVNamespace.DAV]),
|
||||
'sync-level': 1,
|
||||
'sync-token': token,
|
||||
prop: formattedProperties
|
||||
}
|
||||
},
|
||||
{
|
||||
compact: true,
|
||||
spaces: 2,
|
||||
elementNameFn: (name) => {
|
||||
// add namespace to all keys without namespace
|
||||
if (!/^.+:.+/.test(name)) {
|
||||
return `${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:${name}`
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
)
|
||||
return fetch(this.serverUrl + resource, {
|
||||
headers: {
|
||||
'Content-Type': 'application/xml; charset="utf-8"',
|
||||
...this.headers
|
||||
},
|
||||
method: 'REPORT',
|
||||
body: xmlBody
|
||||
})
|
||||
}
|
||||
|
||||
// CalDAV operations
|
||||
|
||||
makeCalendar(resource) {
|
||||
return makeCalendar({
|
||||
url: this.serverUrl + resource,
|
||||
headers: this.headers
|
||||
})
|
||||
}
|
||||
|
||||
createCalendarObject(resource, filename, calendar) {
|
||||
return createCalendarObject({
|
||||
headers: this.headers,
|
||||
calendar: { url: this.serverUrl + resource }, // DAVCalendar
|
||||
filename: filename,
|
||||
iCalString: calendar
|
||||
})
|
||||
}
|
||||
|
||||
postCaldav(resource, vcalendar, originator, recipients) {
|
||||
let localHeaders = { 'content-type': 'text/calendar; charset=utf-8'}
|
||||
|
||||
if (originator)
|
||||
localHeaders.originator = originator
|
||||
if (recipients && recipients.length > 0)
|
||||
localHeaders.recipients = recipients.join(',')
|
||||
|
||||
return fetch(this.serverUrl + resource, {
|
||||
method: 'POST',
|
||||
body: vcalendar,
|
||||
headers: { ...this.headers, ...localHeaders }
|
||||
})
|
||||
}
|
||||
|
||||
propfindEvent(resource) {
|
||||
return propfind({
|
||||
url: this.serverUrl + resource,
|
||||
headers: this.headers,
|
||||
depth: '1',
|
||||
props: [
|
||||
{ name: 'calendar-data', namespace: DAVNamespace.CALDAV }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
calendarMultiGet(resource, filename) {
|
||||
return calendarMultiGet({
|
||||
url: this.serverUrl + resource,
|
||||
headers: this.headers,
|
||||
props: [
|
||||
{ name: 'calendar-data', namespace: DAVNamespace.CALDAV },
|
||||
],
|
||||
objectUrls: [ this.serverUrl + resource + filename ]
|
||||
})
|
||||
}
|
||||
|
||||
principalPropertySearch(resource) {
|
||||
return davRequest({
|
||||
url: `${this.serverUrl}/SOGo/dav`,
|
||||
init: {
|
||||
method: 'REPORT',
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
headers: this.headers,
|
||||
body: {
|
||||
'principal-property-search': {
|
||||
_attributes: getDAVAttribute([
|
||||
DAVNamespace.CALDAV,
|
||||
DAVNamespace.DAV,
|
||||
]),
|
||||
'property-search': [
|
||||
{
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-home-set', namespace: DAVNamespace.CALDAV }]),
|
||||
'match': resource
|
||||
}
|
||||
],
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'displayname', namespace: DAVNamespace.DAV }])
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
syncCollection(resource) {
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'REPORT',
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
headers: this.headers,
|
||||
body: {
|
||||
'sync-collection': {
|
||||
_attributes: getDAVAttribute([
|
||||
DAVNamespace.CALDAV,
|
||||
DAVNamespace.DAV
|
||||
]),
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'calendar-data', namespace: DAVNamespace.CALDAV }]),
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
propfindCaldav(resource, properties, depth = 0) {
|
||||
return this.propfindWebdav(resource, properties, DAVNamespace.CALDAV, { depth: new String(depth) })
|
||||
}
|
||||
|
||||
proppatchCaldav(resource, properties, headers = {}) {
|
||||
return this.proppatchWebdav(resource, properties, DAVNamespace.CALDAV, headers)
|
||||
}
|
||||
|
||||
// CardDAV operations
|
||||
|
||||
getCard(resource, filename) {
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource + filename,
|
||||
init: {
|
||||
method: 'GET',
|
||||
headers: this.headers,
|
||||
body: null
|
||||
},
|
||||
convertIncoming: false
|
||||
})
|
||||
}
|
||||
|
||||
createVCard(resource, filename, card) {
|
||||
return createVCard({
|
||||
headers: this.headers,
|
||||
addressBook: { url: this.serverUrl + resource }, // DAVAddressBook
|
||||
filename,
|
||||
vCardString: card
|
||||
})
|
||||
}
|
||||
|
||||
// MailDAV operations
|
||||
|
||||
mailQueryMaildav(resource, properties, filters = {}, sort, ascending = true) {
|
||||
let formattedFilters = {}
|
||||
if (filters) {
|
||||
if (filters.constructor.toString().includes('Array')) {
|
||||
filters.map(f => {
|
||||
Object.keys(f).map(p => {
|
||||
const pName = `${DAVInverseShort}:${p}`
|
||||
if (!formattedFilters[pName])
|
||||
formattedFilters[pName] = []
|
||||
formattedFilters[pName].push({ _attributes: f[p] })
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
Object.keys(filters).map(p => {
|
||||
const pName = `${DAVInverseShort}:${p}`
|
||||
if (!formattedFilters[pName])
|
||||
formattedFilters[pName] = []
|
||||
formattedFilters[pName].push({ _attributes: filters[p] })
|
||||
})
|
||||
}
|
||||
if (Object.keys(formattedFilters).length) {
|
||||
formattedFilters = {[`${DAVInverseShort}:mail-filters`]: formattedFilters}
|
||||
}
|
||||
}
|
||||
let formattedSort = {}
|
||||
if (sort) {
|
||||
formattedSort = {[`${DAVInverseShort}:sort`]: {
|
||||
_attributes: { order: ascending ? 'ascending' : 'descending' },
|
||||
[sort]: {}
|
||||
}}
|
||||
}
|
||||
return davRequest({
|
||||
url: this.serverUrl + resource,
|
||||
init: {
|
||||
method: 'REPORT',
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
headers: this.headers,
|
||||
body: {
|
||||
[`${DAVInverseShort}:mail-query`]: {
|
||||
_attributes: {
|
||||
...getDAVAttribute([DAVNamespace.DAV]),
|
||||
[`xmlns:${DAVInverseShort}`]: DAVInverse,
|
||||
[`xmlns:${DAVMailHeaderShort}`]: DAVMailHeader
|
||||
},
|
||||
prop: formatProps(properties.map(p => { return { name: p } })),
|
||||
...formattedFilters,
|
||||
...formattedSort
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default WebDAV
|
||||
46
Tests/lib/config.js
Normal file
46
Tests/lib/config.js
Normal file
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
// setup: 4 user are needed: username, superuser, attendee1, attendee1_delegate
|
||||
// superuser must be a sogo superuser...
|
||||
|
||||
hostname: "localhost",
|
||||
port: "20000",
|
||||
username: "mysql1",
|
||||
password: "qwerty",
|
||||
|
||||
superuser: "francis",
|
||||
superuser_password: "qwerty",
|
||||
|
||||
// 'subscriber_username' and 'attendee1' must be the same user
|
||||
subscriber_username: "sogo2",
|
||||
subscriber_password: "sogo2",
|
||||
|
||||
attendee1: "sogo2@inverse.ca",
|
||||
attendee1_username: "sogo2",
|
||||
attendee1_password: "sogo2",
|
||||
|
||||
attendee1_delegate: "sogo3@inverse.ca",
|
||||
attendee1_delegate_username: "sogo3",
|
||||
attendee1_delegate_password: "qwerty",
|
||||
|
||||
resource_no_overbook: "resource1",
|
||||
resource_can_overbook: "resource2",
|
||||
|
||||
// must match username
|
||||
white_listed_attendee: {
|
||||
// "sogo3": "Bob <sogo3@inverse.ca>"
|
||||
//"sogo1": "Bob <sogo1@inverse.ca>"
|
||||
"mysql1": "Bob <mysql1@inverse.ca>"
|
||||
},
|
||||
|
||||
mailserver: "localhost",
|
||||
|
||||
testput_nbrdays: 30,
|
||||
|
||||
sieve_server: "localhost",
|
||||
sieve_port: 4190,
|
||||
|
||||
sogo_user: "francis",
|
||||
sogo_tool_path: "/home/francis/GNUstep/Tools/Admin/sogo-tool",
|
||||
|
||||
webCalendarURL: "http://inverse.ca/sogo-integration-tests/CanadaHolidays.ics"
|
||||
}
|
||||
213
Tests/lib/utilities.js
Normal file
213
Tests/lib/utilities.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
DAVNamespace,
|
||||
davRequest,
|
||||
propfind
|
||||
} from 'tsdav'
|
||||
import ICAL from 'ical.js'
|
||||
|
||||
class TestUtility {
|
||||
constructor(webdav) {
|
||||
this.webdav = webdav
|
||||
this.userInfo = {}
|
||||
}
|
||||
|
||||
async fetchUserInfo(login) {
|
||||
if (!this.userInfo[login]) {
|
||||
const results = await propfind({
|
||||
url: `${this.webdav.serverUrl}/SOGo/dav/${login}/`,
|
||||
props: [
|
||||
{ name: 'displayname', namespace: DAVNamespace.DAV },
|
||||
{ name: 'calendar-timezone', namespace: DAVNamespace.CALDAV },
|
||||
{ name: 'calendar-user-address-set', namespace: DAVNamespace.CALDAV }
|
||||
],
|
||||
depth: '0',
|
||||
headers: this.webdav.headers,
|
||||
})
|
||||
if (results.length != 1) {
|
||||
throw new Error(`Unexpected number of status in profind for user ${login}`)
|
||||
}
|
||||
|
||||
const response = results[0]
|
||||
if (!response.props.calendarUserAddressSet.href.length) {
|
||||
throw new Error(`No address found in calendar-user-address-set for user ${login}`)
|
||||
}
|
||||
|
||||
let displayname = response.props.displayname || ''
|
||||
let email = response.props.calendarUserAddressSet.href[0]
|
||||
let timezone = response.props.calendarTimezone || ''
|
||||
this.userInfo[login] = { displayname, email, timezone }
|
||||
}
|
||||
return this.userInfo[login]
|
||||
}
|
||||
|
||||
formatTemplate(template, vars) {
|
||||
var s = template
|
||||
Object.keys(vars).forEach(k => {
|
||||
s = s.replace(new RegExp(`%\\(${k}\\)`, 'g'), vars[k])
|
||||
})
|
||||
s = s.replace(/%\([^\)]+\)/g, '') // clear all reminding placeholders
|
||||
return s;
|
||||
}
|
||||
|
||||
camelCase(snakeCase) {
|
||||
return snakeCase.replace(/(?:^\w|[A-Z]|\b\w)/g, (char, i) =>
|
||||
i === 0 ? char.toLowerCase() : char.toUpperCase(),
|
||||
)
|
||||
.replace(/[\s\-_]+/g, '')
|
||||
}
|
||||
|
||||
setupRights(resource, username, rights) {
|
||||
const action = (typeof rights == 'undefined') ? 'remove-user' : 'set-roles'
|
||||
return davRequest({
|
||||
url: `${this.webdav.serverUrl}${resource}`,
|
||||
init: {
|
||||
method: 'POST',
|
||||
headers: this.webdav.headers,
|
||||
body: {
|
||||
'acl-query': {
|
||||
_attributes: { xmlns: 'urn:inverse:params:xml:ns:inverse-dav' },
|
||||
[action]: {
|
||||
_attributes: { user: username.replace('<', '<').replace('>', '>') },
|
||||
...rights
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setupCalendarRights(resource, username, rights) {
|
||||
let sogoRights = {}
|
||||
if (rights.c)
|
||||
sogoRights.ObjectCreator = {}
|
||||
if (rights.d)
|
||||
sogoRights.ObjectEraser = {}
|
||||
|
||||
const classes = {
|
||||
pu: 'Public',
|
||||
pr: 'Private',
|
||||
co: 'Confidential'
|
||||
}
|
||||
const rightsTable = {
|
||||
v: 'Viewer',
|
||||
d: 'DAndTViewer',
|
||||
m: 'Modifier',
|
||||
r: 'Responder'
|
||||
}
|
||||
Object.keys(classes).forEach(c => {
|
||||
if (rights[c]) {
|
||||
const right = rights[c]
|
||||
const sogoRight = `${classes[c]}${rightsTable[right]}`
|
||||
sogoRights[sogoRight] = {}
|
||||
}
|
||||
})
|
||||
|
||||
return this.setupRights(resource, username, sogoRights)
|
||||
}
|
||||
|
||||
setupAddressBookRights(resource, username, rights) {
|
||||
let sogoRights = {}
|
||||
|
||||
const rightsTable = {
|
||||
c: 'ObjectCreator',
|
||||
d: 'ObjectEraser',
|
||||
v: 'ObjectViewer',
|
||||
e: 'ObjectEditor'
|
||||
}
|
||||
Object.keys(rights).forEach(r => {
|
||||
if (rightsTable[r]) {
|
||||
sogoRights[rightsTable[r]] = {}
|
||||
}
|
||||
})
|
||||
|
||||
return this.setupRights(resource, username, sogoRights)
|
||||
}
|
||||
|
||||
_subscriptionOperation(resource, subscribers, operation) {
|
||||
return davRequest({
|
||||
url: `${this.webdav.serverUrl}${resource}`,
|
||||
init: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/xml; charset="utf-8"',
|
||||
...this.webdav.headers
|
||||
},
|
||||
body: {
|
||||
[operation]: {
|
||||
_attributes: {
|
||||
xmlns: 'urn:inverse:params:xml:ns:inverse-dav',
|
||||
users: subscribers.join(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
subscribe(resource, subscribers) {
|
||||
return this._subscriptionOperation(resource, subscribers, 'subscribe')
|
||||
}
|
||||
|
||||
unsubscribe(resource, subscribers) {
|
||||
return this._subscriptionOperation(resource, subscribers, 'unsubscribe')
|
||||
}
|
||||
|
||||
versitDict(component) {
|
||||
const comp = ICAL.Component.fromString(component)
|
||||
let props = {}
|
||||
|
||||
for (const prop of comp.getAllProperties()) {
|
||||
props[prop.name] = prop.toICALString()
|
||||
}
|
||||
for (const subcomp of comp.getAllSubcomponents()) {
|
||||
for (const prop of subcomp.getAllProperties()) {
|
||||
props[prop.name] = prop.toICALString()
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
componentsAreEqual(comp1, comp2) {
|
||||
const props1 = this.versitDict(comp1)
|
||||
const props2 = this.versitDict(comp2)
|
||||
for (const prop of Object.keys(props1)) {
|
||||
if (props1[prop] != props2[prop]) {
|
||||
console.debug(`Difference detected in ${prop}:\n\t1: ${props1[prop]}\n\t2: ${props2[prop]}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
createDateTimeProperty(propertyName, dateObject = new Date()) {
|
||||
let property = new ICAL.Property(propertyName)
|
||||
property.setParameter('tzid', 'America/Toronto')
|
||||
property.setValue(ICAL.Time.fromJSDate(dateObject))
|
||||
|
||||
return property
|
||||
}
|
||||
|
||||
createCalendar(summary = 'test event', uid = 'test', transp = 'OPAQUE') {
|
||||
const vcalendar = new ICAL.Component('vcalendar')
|
||||
const vevent = new ICAL.Component('vevent')
|
||||
const now = new Date()
|
||||
const later = new Date(now.getTime() + 1000*60*60) // event lasts one hour
|
||||
|
||||
vcalendar.addSubcomponent(vevent)
|
||||
vevent.addPropertyWithValue('uid', uid)
|
||||
vevent.addPropertyWithValue('summary', summary)
|
||||
vevent.addPropertyWithValue('transp', transp)
|
||||
vevent.addProperty(this.createDateTimeProperty('dtstart', now))
|
||||
vevent.addProperty(this.createDateTimeProperty('dtend', later))
|
||||
vevent.addProperty(this.createDateTimeProperty('dtstamp', now))
|
||||
vevent.addProperty(this.createDateTimeProperty('last-modified', now))
|
||||
vevent.addProperty(this.createDateTimeProperty('created', now))
|
||||
vevent.addPropertyWithValue('class', 'PUBLIC')
|
||||
vevent.addPropertyWithValue('sequence', '0')
|
||||
|
||||
return vcalendar
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TestUtility
|
||||
19
Tests/package.json
Normal file
19
Tests/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "tests",
|
||||
"description": "This directory holds automated tests for SOGo.",
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "jasmine"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"cookie": "^0.4.1",
|
||||
"cross-fetch": "^3.1.4",
|
||||
"esm": "^3.2.25",
|
||||
"ical.js": "^1.4.0",
|
||||
"jasmine": "^3.10.0",
|
||||
"tsdav": "^1.1.5"
|
||||
}
|
||||
}
|
||||
61
Tests/spec/CalDAVPropertiesSpec.js
Normal file
61
Tests/spec/CalDAVPropertiesSpec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
|
||||
describe('read and set calendar properties', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-properties/`
|
||||
|
||||
beforeEach(async function() {
|
||||
await webdav.makeCalendar(resource)
|
||||
})
|
||||
|
||||
afterEach(async function() {
|
||||
await webdav.deleteObject(resource)
|
||||
})
|
||||
|
||||
// CalDAVPropertiesTest
|
||||
|
||||
it("propfind", async function() {
|
||||
const [result] = await webdav.propfindCaldav(resource, ['schedule-calendar-transp'])
|
||||
const { raw: { multistatus: { response: { propstat: { status, prop }}}}} = result
|
||||
expect(status)
|
||||
.withContext('schedule-calendar-transp profind is successful')
|
||||
.toBe('HTTP/1.1 200 OK')
|
||||
expect(Object.keys(prop).length)
|
||||
.withContext('schedule-calendar-transp has one element only')
|
||||
.toBe(1)
|
||||
expect(Object.keys(prop.scheduleCalendarTransp).includes('opaque'))
|
||||
.withContext('schedule-calendar-transp is "opaque" on new')
|
||||
.toBeTrue()
|
||||
})
|
||||
|
||||
it("proppatch", async function() {
|
||||
let newValueNode
|
||||
let results
|
||||
|
||||
newValueNode = { 'thisvaluedoesnotexist': {} }
|
||||
results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode})
|
||||
expect(results.length)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext('Setting an invalid transparency is refused')
|
||||
.toBe(400)
|
||||
|
||||
newValueNode = { 'transparent': {} }
|
||||
results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode})
|
||||
expect(results.length)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`Setting transparency to ${newValueNode} is successful`)
|
||||
.toBe(207)
|
||||
|
||||
newValueNode = { 'opaque': {} }
|
||||
results = await webdav.proppatchCaldav(resource, {'schedule-calendar-transp': newValueNode})
|
||||
expect(results.length)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`Setting transparency to ${newValueNode} is successful`)
|
||||
.toBe(207)
|
||||
|
||||
})
|
||||
})
|
||||
181
Tests/spec/CardDAVSpec.js
Normal file
181
Tests/spec/CardDAVSpec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
import {
|
||||
DAVNamespace,
|
||||
DAVNamespaceShorthandMap,
|
||||
davRequest,
|
||||
formatProps,
|
||||
getDAVAttribute,
|
||||
propfind
|
||||
} from 'tsdav'
|
||||
|
||||
const cards = {
|
||||
'new.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:New;Carte
|
||||
FN:Carte 'new'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`,
|
||||
'new-modified.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:New;Carte modifiee
|
||||
FN:Carte modifiee 'new'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`
|
||||
}
|
||||
|
||||
describe('CardDAV extensions', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const utility = new TestUtility(webdav)
|
||||
|
||||
const resource = `/SOGo/dav/${config.username}/Contacts/test-carddav/`
|
||||
|
||||
const _putCard = async function(client, filename, expectedCode, realCard) {
|
||||
const card = cards[realCard || filename]
|
||||
if (!card)
|
||||
throw new Error(`Card ${realCard || filename} is unknown`)
|
||||
const response = await client.createVCard(resource, filename, card)
|
||||
expect(response.status).toBe(expectedCode)
|
||||
}
|
||||
|
||||
beforeAll(async function() {
|
||||
await webdav.deleteObject(resource)
|
||||
await webdav.makeCollection(resource)
|
||||
for (let key of Object.keys(cards)) {
|
||||
await _putCard(webdav, key, 201)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(async function() {
|
||||
await webdav_su.deleteObject(resource)
|
||||
})
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6352#section-8.7
|
||||
it("supports for addressbook-multiget", async function() {
|
||||
const hrefs = Object.keys(cards).map(c => `${resource}${c}`)
|
||||
const response = await davRequest({
|
||||
url: webdav.serverUrl + resource,
|
||||
init: {
|
||||
method: 'REPORT',
|
||||
namespace: DAVNamespaceShorthandMap[DAVNamespace.CARDDAV],
|
||||
headers: { ...webdav.headers, depth: '0' },
|
||||
body: {
|
||||
'addressbook-multiget': {
|
||||
_attributes: getDAVAttribute([
|
||||
DAVNamespace.CARDDAV,
|
||||
DAVNamespace.DAV
|
||||
]),
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'address-data', namespace: DAVNamespace.CARDDAV }]),
|
||||
[`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:href`]: hrefs
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
expect(response.length).toBe(2)
|
||||
for (let r of response) {
|
||||
const [name, ...rest] = r.href.split('/').reverse()
|
||||
expect(r.status)
|
||||
.withContext(`HTTP status code of addressbook-multiget`)
|
||||
.toEqual(207)
|
||||
expect(utility.componentsAreEqual(r.props.addressData, cards[name]))
|
||||
.withContext(`Cards returned in addressbook-multiget`)
|
||||
.toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6352#section-8.7
|
||||
it("supports for propfind on global addressbook", async function() {
|
||||
const hrefs = Object.keys(cards).map(c => `${resource}${c}`)
|
||||
const response = await webdav.propfindWebdav(resource, ['getetag'], DAVNamespace.DAV, { depth: '1'})
|
||||
})
|
||||
|
||||
it("TEST", async function() {
|
||||
// const response = await webdav_su.propfindWebdav(`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`, ['getetag', 'displayname'], DAVNamespace.DAV, { depth: '1'})
|
||||
// const response = await davRequest({
|
||||
// url: webdav_su.serverUrl + `/SOGo/dav/${config.superuser}/Contacts/personal/`,
|
||||
// init: {
|
||||
// method: 'REPORT',
|
||||
// namespace: DAVNamespaceShorthandMap[DAVNamespace.DAV],
|
||||
// headers: webdav_su.headers,
|
||||
// body: {
|
||||
// 'addressbook-multiget': {
|
||||
// _attributes: getDAVAttribute([
|
||||
// DAVNamespace.DAV
|
||||
// ]),
|
||||
// [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([{ name: 'displayname', namespace: DAVNamespace.DAV }]),
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
// const hrefs = [`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`]
|
||||
// const response = await davRequest({
|
||||
// url: webdav_su.serverUrl + `/SOGo/dav/${config.superuser}/Contacts/personal/`,
|
||||
// init: {
|
||||
// method: 'REPORT',
|
||||
// namespace: DAVNamespaceShorthandMap[DAVNamespace.CARDDAV],
|
||||
// headers: { ...webdav_su.headers, depth: '0' },
|
||||
// body: {
|
||||
// 'addressbook-multiget': {
|
||||
// _attributes: getDAVAttribute([
|
||||
// DAVNamespace.CARDDAV,
|
||||
// DAVNamespace.DAV
|
||||
// ]),
|
||||
// [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:prop`]: formatProps([
|
||||
// { name: 'getetag', namespace: DAVNamespace.DAV },
|
||||
// { name: 'displayname', namespace: DAVNamespace.CARDDAV }]),
|
||||
// [`${DAVNamespaceShorthandMap[DAVNamespace.DAV]}:href`]: hrefs
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// })
|
||||
// const response = await webdav_su.propfindWebdav(`/SOGo/dav/${config.superuser}/Contacts/personal/66AF-61E87800-1-3FD13600.vcf`, [], DAVNamespace.DAV, { depth: '1'})
|
||||
// console.debug(response)
|
||||
})
|
||||
})
|
||||
38
Tests/spec/ConfigSpec.js
Normal file
38
Tests/spec/ConfigSpec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import config from '../lib/config'
|
||||
|
||||
describe('config tests', function() {
|
||||
|
||||
it('required configuration parameters', async function() {
|
||||
expect(config.hostname)
|
||||
.withContext(`Config 'hostname'`)
|
||||
.toBeDefined()
|
||||
expect(config.username)
|
||||
.withContext(`Config 'username'`)
|
||||
.toBeDefined()
|
||||
expect(config.subscriber_username)
|
||||
.withContext(`Config 'subscriber_username'`)
|
||||
.toBeDefined()
|
||||
expect(config.attendee1)
|
||||
.withContext(`Config 'attendee1'`)
|
||||
.toBeDefined()
|
||||
expect(config.attendee1_delegate)
|
||||
.withContext(`Config 'attendee1_delegate'`)
|
||||
.toBeDefined()
|
||||
expect(config.mailserver)
|
||||
.withContext(`Config 'mailserver'`)
|
||||
.toBeDefined()
|
||||
|
||||
expect(config.subscriber_username)
|
||||
.withContext(`Config 'subscriber_username' and 'attendee1_username'`)
|
||||
.toEqual(config.attendee1_username)
|
||||
|
||||
let userHash = {}
|
||||
const userList = [config.username, config.subscriber_username, config.attendee1_delegate_username]
|
||||
for (let user of userList) {
|
||||
expect(userHash[user])
|
||||
.withContext(`username, subscriber_username, attendee1_delegate_username must all be different users ('${user}')`)
|
||||
.toBeUndefined()
|
||||
userHash[user] = true
|
||||
}
|
||||
})
|
||||
})
|
||||
242
Tests/spec/DAVAddressBookAclSpec.js
Normal file
242
Tests/spec/DAVAddressBookAclSpec.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
describe('create, read, modify, delete tasks for regular user', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password)
|
||||
const utility = new TestUtility(webdav)
|
||||
|
||||
const cards = {
|
||||
'new.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:New;Carte
|
||||
FN:Carte 'new'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`,
|
||||
'old.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:Old;Carte
|
||||
FN:Carte 'old'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`,
|
||||
'new-modified.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:New;Carte modifiee
|
||||
FN:Carte modifiee 'new'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`,
|
||||
'old-modified.vcf': `BEGIN:VCARD
|
||||
VERSION:3.0
|
||||
PRODID:-//Inverse//Card Generator//EN
|
||||
UID:NEWTESTCARD
|
||||
N:Old;Carte modifiee
|
||||
FN:Carte modifiee 'old'
|
||||
ORG:societe;service
|
||||
NICKNAME:surnom
|
||||
ADR;TYPE=work:adr2 societe;;adr societe;ville societe;etat soc;code soc;pays soc
|
||||
ADR;TYPE=home:rue perso 2;;rue perso;ville perso;etat perso;code post perso;pays perso
|
||||
TEL;TYPE=work:+1 514 123-3372
|
||||
TEL;TYPE=home:tel dom
|
||||
TEL;TYPE=cell:portable
|
||||
TEL;TYPE=fax:fax
|
||||
TEL;TYPE=pager:pager
|
||||
X-MOZILLA-HTML:FALSE
|
||||
EMAIL;TYPE=work:address.email@domaine.ca
|
||||
EMAIL;TYPE=home:address.email@domaine2.com
|
||||
URL;TYPE=home:web perso
|
||||
TITLE:fonction
|
||||
URL;TYPE=work:page soc
|
||||
CUSTOM1:divers1
|
||||
CUSTOM2:divers2
|
||||
CUSTOM3:divers3
|
||||
CUSTOM4:divers4
|
||||
NOTE:Remarque
|
||||
X-AIM:pseudo aim
|
||||
END:VCARD`
|
||||
}
|
||||
|
||||
const sogoRights = {
|
||||
c: 'ObjectCreator',
|
||||
d: 'ObjectEraser',
|
||||
v: 'ObjectViewer',
|
||||
e: 'ObjectEditor'
|
||||
}
|
||||
|
||||
const resource = `/SOGo/dav/${config.username}/Contacts/test-dav-acl/`
|
||||
|
||||
const _putCard = async function(client, filename, expectedCode, realCard) {
|
||||
const card = cards[realCard || filename]
|
||||
if (!card)
|
||||
throw new Error(`Card ${realCard || filename} is unknown`)
|
||||
const response = await client.createVCard(resource, filename, card)
|
||||
expect(response.status).toBe(expectedCode)
|
||||
}
|
||||
|
||||
const _getCard = async function(client, filename, expectedCode) {
|
||||
const [{ status }] = await client.getCard(resource, filename)
|
||||
expect(status).toBe(expectedCode)
|
||||
}
|
||||
|
||||
const _deleteCard = async function(client, filename, expectedCode = 204) {
|
||||
const response = await client.deleteObject(resource + filename)
|
||||
expect(response.status)
|
||||
.withContext('HTTP status code when deleting a card')
|
||||
.toBe(expectedCode)
|
||||
}
|
||||
|
||||
const _testView = async function(rights) {
|
||||
let expectedCode = 403
|
||||
if (rights && (rights.v || rights.e)) {
|
||||
expectedCode = 200
|
||||
}
|
||||
await _getCard(webdav_subscriber, 'old.vcf', expectedCode)
|
||||
}
|
||||
|
||||
const _testCreate = async function(rights) {
|
||||
let expectedCode
|
||||
if (rights && rights.c)
|
||||
expectedCode = 201
|
||||
else
|
||||
expectedCode = 403
|
||||
await _putCard(webdav_subscriber, 'new.vcf', expectedCode)
|
||||
}
|
||||
|
||||
const _testModify = async function(rights) {
|
||||
let expectedCode
|
||||
if (rights && rights.e)
|
||||
expectedCode = 204
|
||||
else
|
||||
expectedCode = 403
|
||||
await _putCard(webdav_subscriber, 'old.vcf', expectedCode, 'old-modified.vcf')
|
||||
}
|
||||
|
||||
const _testDelete = async function(rights) {
|
||||
let expectedCode = 403
|
||||
if (rights && rights.d) {
|
||||
expectedCode = 204
|
||||
}
|
||||
await _deleteCard(webdav_subscriber, 'old.vcf', expectedCode)
|
||||
}
|
||||
|
||||
const _testRights = async function(rights) {
|
||||
const results = await utility.setupAddressBookRights(resource, config.subscriber_username, rights)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].status).toBe(204)
|
||||
await _testCreate(rights)
|
||||
await _testView(rights)
|
||||
await _testModify(rights)
|
||||
await _testDelete(rights)
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await webdav.deleteObject(resource)
|
||||
await webdav.makeCollection(resource)
|
||||
await _putCard(webdav, 'old.vcf', 201)
|
||||
})
|
||||
|
||||
afterEach(async function() {
|
||||
await webdav_su.deleteObject(resource)
|
||||
})
|
||||
|
||||
// DAVAddressBookAclTest
|
||||
|
||||
it("'view' only", async function() {
|
||||
await _testRights({ v: true })
|
||||
})
|
||||
|
||||
it("'edit' only", async function() {
|
||||
await _testRights({ e: true })
|
||||
})
|
||||
|
||||
it("'create' only", async function() {
|
||||
await _testRights({ c: true })
|
||||
})
|
||||
|
||||
it("'delete' only", async function() {
|
||||
await _testRights({ d: true })
|
||||
})
|
||||
|
||||
it("'create', 'delete'", async function() {
|
||||
await _testRights({ c: true, d: true })
|
||||
})
|
||||
|
||||
it("'view', 'delete'", async function() {
|
||||
await _testRights({ v: true, d: true })
|
||||
})
|
||||
|
||||
it("'edit', 'create'", async function() {
|
||||
await _testRights({ c: true, e: true })
|
||||
})
|
||||
|
||||
it("'edit', 'delete'", async function() {
|
||||
await _testRights({ d: true, e: true })
|
||||
})
|
||||
})
|
||||
452
Tests/spec/DAVCalendarAclSpec.js
Normal file
452
Tests/spec/DAVCalendarAclSpec.js
Normal file
@@ -0,0 +1,452 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
describe('create, read, modify, delete events for regular user', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password)
|
||||
const utility = new TestUtility(webdav)
|
||||
|
||||
const event_template = `BEGIN:VCALENDAR
|
||||
PRODID:-//Inverse//Event Generator//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:0
|
||||
TRANSP:OPAQUE
|
||||
UID:12345-%(class)-%(filename)
|
||||
SUMMARY:%(class) event (orig. title)
|
||||
DTSTART:20090805T100000Z
|
||||
DTEND:20090805T140000Z
|
||||
CLASS:%(class)
|
||||
DESCRIPTION:%(class) description
|
||||
LOCATION:location
|
||||
%(organizer_line)%(attendee_line)CREATED:20090805T100000Z
|
||||
DTSTAMP:20090805T100000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`
|
||||
|
||||
const task_template = `BEGIN:VCALENDAR
|
||||
PRODID:-//Inverse//Event Generator//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTODO
|
||||
CREATED:20100122T201440Z
|
||||
LAST-MODIFIED:20100201T175246Z
|
||||
DTSTAMP:20100201T175246Z
|
||||
UID:12345-%(class)-%(filename)
|
||||
SUMMARY:%(class) event (orig. title)
|
||||
CLASS:%(class)
|
||||
DESCRIPTION:%(class) description
|
||||
STATUS:IN-PROCESS
|
||||
PERCENT-COMPLETE:0
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
const resource = `/SOGo/dav/${config.username}/Calendar/test-dav-acl/`
|
||||
const classToICSClass = {
|
||||
'pu': 'PUBLIC',
|
||||
'pr': 'PRIVATE',
|
||||
'co': 'CONFIDENTIAL'
|
||||
}
|
||||
|
||||
let user
|
||||
|
||||
const _checkViewEventRight = function(operation, event, eventClass, right) {
|
||||
if (right) {
|
||||
expect(event)
|
||||
.withContext(`Returned event during operation '${operation}'`)
|
||||
.toBeTruthy()
|
||||
if (['v', 'r', 'm'].includes(right)) {
|
||||
const iscClass = classToICSClass[eventClass]
|
||||
const expectedEvent = utility.formatTemplate(event_template, {
|
||||
'class': iscClass,
|
||||
'filename': `${iscClass.toLowerCase()}-event.ics`
|
||||
})
|
||||
expect(event).toBe(expectedEvent)
|
||||
}
|
||||
else if (right == 'd') {
|
||||
_testEventIsSecureVersion(eventClass, event)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Right '${right} is not supported`)
|
||||
}
|
||||
}
|
||||
else {
|
||||
expect(event).toBeFalsy()
|
||||
}
|
||||
}
|
||||
|
||||
const _currentUserPrivilegeSet = async function(resource, expectedCode = 207) {
|
||||
const results = await webdav_subscriber.currentUserPrivilegeSet(resource)
|
||||
expect(results.length).toBe(1)
|
||||
const response = results[0]
|
||||
expect(response.status).toBe(expectedCode)
|
||||
let privileges = []
|
||||
if (expectedCode < 300) {
|
||||
privileges = response.props.currentUserPrivilegeSet.privilege.map(o => {
|
||||
return Object.keys(o)[0]
|
||||
})
|
||||
}
|
||||
return privileges
|
||||
}
|
||||
|
||||
const _deleteEvent = async function(client, filename, expectedCode = 204) {
|
||||
const response = await client.deleteObject(resource + filename)
|
||||
expect(response.status).toBe(expectedCode)
|
||||
}
|
||||
|
||||
const _getEvent = async function(eventClass, isInvitation = false) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase()
|
||||
const filename = (isInvitation ? `invitation-${iscClass}` : iscClass) + '-event.ics'
|
||||
const [{ status, raw = '' }] = await webdav_subscriber.getObject(resource, filename)
|
||||
|
||||
if (status == 200)
|
||||
return raw.replace(/\r\n/g,'\n')
|
||||
else
|
||||
return undefined
|
||||
}
|
||||
|
||||
const _multigetEvent = async function(eventClass) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase()
|
||||
const filename = `${iscClass}-event.ics`
|
||||
let event = undefined
|
||||
const results = await webdav_subscriber.calendarMultiGet(resource, filename)
|
||||
if (results.status !== 404) {
|
||||
results.find(o => {
|
||||
if (o.href == resource + filename) {
|
||||
const { props: { calendarData = '' } } = o
|
||||
event = calendarData.replace(/\r\n/g,'\n')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
const _propfindEvent = async function(eventClass) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase()
|
||||
const filename = `${iscClass}-event.ics`
|
||||
const results = await webdav_subscriber.propfindEvent(resource + filename)
|
||||
let event = undefined
|
||||
if (results.status !== 404) {
|
||||
results.find(o => {
|
||||
if (o.href == resource + filename) {
|
||||
const { props: { calendarData = '' } } = o
|
||||
event = calendarData.replace(/\r\n/g,'\n')
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
const _putEvent = async function(client, filename, eventClass = 'PUBLIC', expectedCode = 201, organizer, attendee, partstat = 'NEEDS-ACTION') {
|
||||
const organizer_line = organizer ? `ORGANIZER:${organizer}\n` : ''
|
||||
const attendee_line = attendee ? `ATTENDEE;PARTSTAT=${partstat}:${attendee}\n` : ''
|
||||
const event = utility.formatTemplate(event_template, {
|
||||
'class': eventClass,
|
||||
'filename': filename,
|
||||
organizer_line,
|
||||
attendee_line
|
||||
})
|
||||
const response = await client.createCalendarObject(resource, filename, event)
|
||||
expect(response.status).toBe(expectedCode)
|
||||
}
|
||||
|
||||
const _webdavSyncEvent = async function(eventClass) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase()
|
||||
const filename = `${iscClass}-event.ics`
|
||||
let event = undefined
|
||||
const results = await webdav_subscriber.syncCollection(resource)
|
||||
if (results.status !== 404) {
|
||||
results.find(o => {
|
||||
if (o.href == resource + filename) {
|
||||
const { props: { calendarData = '' } } = o
|
||||
event = calendarData.length ? calendarData.replace(/\r\n/g,'\n') : undefined
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
const _testCreate = async function(rights) {
|
||||
let expectedCode
|
||||
if (rights.c)
|
||||
expectedCode = 201
|
||||
else if (Object.keys(rights).length === 0)
|
||||
expectedCode = 404
|
||||
else
|
||||
expectedCode = 403
|
||||
return _putEvent(webdav_subscriber, 'creation-test.ics', 'PUBLIC', expectedCode)
|
||||
}
|
||||
|
||||
const _testCollectionDAVAcl = async function(rights) {
|
||||
let expectedPrivileges = []
|
||||
if (Object.keys(rights).length > 0) {
|
||||
expectedPrivileges.push('read', 'readCurrentUserPrivilegeSet', 'readFreeBusy')
|
||||
}
|
||||
if (rights.c) {
|
||||
expectedPrivileges.push(
|
||||
'bind',
|
||||
'writeContent',
|
||||
'schedule',
|
||||
'schedulePost',
|
||||
'schedulePostVevent',
|
||||
'schedulePostVtodo',
|
||||
'schedulePostVjournal',
|
||||
'schedulePostVfreebusy',
|
||||
'scheduleDeliver',
|
||||
'scheduleDeliverVevent',
|
||||
'scheduleDeliverVtodo',
|
||||
'scheduleDeliverVjournal',
|
||||
'scheduleDeliverVfreebusy',
|
||||
'scheduleRespond',
|
||||
'scheduleRespondVevent',
|
||||
'scheduleRespondVtodo'
|
||||
)
|
||||
}
|
||||
if (rights.d) {
|
||||
expectedPrivileges.push('unbind')
|
||||
}
|
||||
const expectedCode = (expectedPrivileges.length == 0) ? 404 : 207
|
||||
const privileges = await _currentUserPrivilegeSet(resource, expectedCode)
|
||||
|
||||
// When comparing privileges on DAV collection, we remove all 'default'
|
||||
// privileges on the collection.
|
||||
for (const c of ['Public', 'Private', 'Confidential']) {
|
||||
for (const r of ['viewdant', 'viewwhole', 'modify', 'respondto']) {
|
||||
const i = privileges.indexOf(`${r}${c}Records`)
|
||||
if (i >= 0) {
|
||||
privileges.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (const privilege of ['read', 'readCurrentUserPrivilegeSet', 'readFreeBusy']) {
|
||||
for (const expectedPrivilege of expectedPrivileges) {
|
||||
expect(privileges).toContain(expectedPrivilege)
|
||||
}
|
||||
}
|
||||
|
||||
const _testEventIsSecureVersion = function(eventClass, event) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase().replace(/^\w/, c => c.toUpperCase())
|
||||
const expectedDict = {
|
||||
version: 'VERSION:2.0',
|
||||
prodid: 'PRODID:-//Inverse//Event Generator//EN',
|
||||
summary: `SUMMARY:(${iscClass} event)`,
|
||||
dtstart: 'DTSTART:20090805T100000Z',
|
||||
dtend: 'DTEND:20090805T140000Z',
|
||||
dtstamp: 'DTSTAMP:20090805T100000Z',
|
||||
'x-sogo-secure': 'X-SOGO-SECURE:YES'
|
||||
}
|
||||
const eventDict = utility.versitDict(event)
|
||||
// Ignore UID
|
||||
for (const key of Object.keys(eventDict).filter(k => k !== 'uid')) {
|
||||
expect(expectedDict[key])
|
||||
.withContext(`Key ${key} of secure event is expected`)
|
||||
.toBeTruthy()
|
||||
if (expectedDict[key])
|
||||
expect(expectedDict[key])
|
||||
.withContext(`Value of key ${key} of secure event is valid`)
|
||||
.toBe(eventDict[key])
|
||||
}
|
||||
for (const key of Object.keys(expectedDict)) {
|
||||
expect(eventDict[key])
|
||||
.withContext(`Key ${key} of secure event is present`)
|
||||
.toBeTruthy()
|
||||
}
|
||||
}
|
||||
|
||||
const _testModify = async function(eventClass, right, errorCode) {
|
||||
const iscClass = classToICSClass[eventClass]
|
||||
const filename = `${iscClass.toLowerCase()}-event.ics`
|
||||
let expectedCode = errorCode
|
||||
if (['r', 'm'].includes(right))
|
||||
expectedCode = 204
|
||||
return _putEvent(webdav_subscriber, filename, iscClass, expectedCode)
|
||||
}
|
||||
|
||||
const _testRespondTo = async function(eventClass, right, errorCode) {
|
||||
const iscClass = classToICSClass[eventClass]
|
||||
const filename = `invitation-${iscClass.toLowerCase()}-event.ics`
|
||||
let expectedCode = errorCode
|
||||
if (['r', 'm'].includes(right))
|
||||
expectedCode = 204
|
||||
|
||||
await _putEvent(webdav, filename, iscClass, 201, 'mailto:nobody@somewhere.com', user.email, 'NEEDS-ACTION')
|
||||
|
||||
// here we only do 'passive' validation: if a user has a "respond to"
|
||||
// right, only the attendee entry will me modified. The change of
|
||||
// organizer must thus be silently ignored below.
|
||||
await _putEvent(webdav_subscriber, filename, iscClass, expectedCode, 'mailto:someone@nowhere.com', user.email, 'ACCEPTED')
|
||||
|
||||
if (expectedCode == 204) {
|
||||
const attendee_line = `ATTENDEE;PARTSTAT=ACCEPTED:${user.email}\n`
|
||||
let expectedEvent
|
||||
if (right == 'r') {
|
||||
expectedEvent = utility.formatTemplate(event_template, {
|
||||
'class': iscClass,
|
||||
'filename': filename,
|
||||
organizer_line: 'ORGANIZER;CN=nobody@somewhere.com:mailto:nobody@somewhere.com\n',
|
||||
attendee_line
|
||||
})
|
||||
}
|
||||
else {
|
||||
expectedEvent = utility.formatTemplate(event_template, {
|
||||
'class': iscClass,
|
||||
'filename': filename,
|
||||
organizer_line: 'ORGANIZER;CN=someone@nowhere.com:mailto:someone@nowhere.com\n',
|
||||
attendee_line
|
||||
})
|
||||
}
|
||||
const event = await _getEvent(eventClass, true)
|
||||
expect(utility.componentsAreEqual(expectedEvent, event))
|
||||
.withContext('Calendars of organizer and attendee are identical')
|
||||
.toBe(true)
|
||||
}
|
||||
}
|
||||
|
||||
const _testEventDAVAcl = async function(eventClass, right, errorCode) {
|
||||
const iscClass = classToICSClass[eventClass].toLowerCase()
|
||||
for (const suffix of ['event', 'task']) {
|
||||
const filename = `${iscClass}-${suffix}.ics`
|
||||
let expectedCode = errorCode
|
||||
let expectedPrivileges = []
|
||||
if (right) {
|
||||
expectedCode = 207
|
||||
expectedPrivileges.push('readCurrentUserPrivilegeSet', 'viewDateAndTime', 'read')
|
||||
if (right != 'd') {
|
||||
expectedPrivileges.push('viewWholeComponent')
|
||||
if (right != 'v') {
|
||||
expectedPrivileges.push('respondToComponent', 'writeContent')
|
||||
if (right != 'r') {
|
||||
expectedPrivileges.push('writeProperties', 'write')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const privileges = await _currentUserPrivilegeSet(resource + filename, expectedCode)
|
||||
if (errorCode != expectedCode) {
|
||||
for (const expectedPrivilege of expectedPrivileges) {
|
||||
expect(privileges).toContain(expectedPrivilege)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const _testEventRight = async function(eventClass, rights) {
|
||||
const right = Object.keys(rights).includes(eventClass) ? rights[eventClass] : undefined
|
||||
|
||||
let event
|
||||
|
||||
event = await _getEvent(eventClass)
|
||||
_checkViewEventRight('GET', event, eventClass, right)
|
||||
|
||||
event = await _propfindEvent(eventClass)
|
||||
_checkViewEventRight('PROPFIND', event, eventClass, right)
|
||||
|
||||
event = await _multigetEvent(eventClass)
|
||||
_checkViewEventRight('multiget', event, eventClass, right)
|
||||
|
||||
event = await _webdavSyncEvent(eventClass)
|
||||
_checkViewEventRight('webdav-sync', event, eventClass, right)
|
||||
|
||||
const errorCode = (Object.keys(rights).length > 0) ? 403 : 404
|
||||
await _testModify(eventClass, right, errorCode)
|
||||
await _testRespondTo(eventClass, right, errorCode)
|
||||
await _testEventDAVAcl(eventClass, right, errorCode)
|
||||
}
|
||||
|
||||
const _testDelete = async function(rights) {
|
||||
let expectedCode = 403
|
||||
if (rights && rights.d) {
|
||||
expectedCode = 204
|
||||
}
|
||||
else if (Object.keys(rights) == 0) {
|
||||
expectedCode = 404
|
||||
}
|
||||
for (const eventClass of Object.values(classToICSClass)) {
|
||||
await _deleteEvent(webdav_subscriber, `${eventClass.toLocaleLowerCase()}-event.ics`, expectedCode)
|
||||
}
|
||||
}
|
||||
|
||||
const _testRights = async function(rights) {
|
||||
const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`Setup rights (${JSON.stringify(rights)}) on ${resource}`)
|
||||
.toBe(204)
|
||||
await _testCreate(rights)
|
||||
await _testCollectionDAVAcl(rights)
|
||||
await _testEventRight('pu', rights)
|
||||
await _testEventRight('pr', rights)
|
||||
await _testEventRight('co', rights)
|
||||
await _testDelete(rights)
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
user = await utility.fetchUserInfo(config.username)
|
||||
await webdav.deleteObject(resource)
|
||||
await webdav.makeCalendar(resource)
|
||||
for (const c of Object.values(classToICSClass)) {
|
||||
// Create event for each class
|
||||
const eventFilename = `${c.toLowerCase()}-event.ics`
|
||||
const event = utility.formatTemplate(event_template, {
|
||||
'class': c,
|
||||
'filename': eventFilename
|
||||
})
|
||||
let response = await webdav.createCalendarObject(resource, eventFilename, event)
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status when creating event with ${c} class`)
|
||||
.toBe(201)
|
||||
// Create task for each class
|
||||
const taskFilename = `${c.toLowerCase()}-task.ics`
|
||||
const task = utility.formatTemplate(task_template, {
|
||||
'class': c,
|
||||
'filename': taskFilename
|
||||
})
|
||||
response = await webdav.createCalendarObject(resource, taskFilename, task)
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status when creating task with ${c} class`)
|
||||
.toBe(201)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async function() {
|
||||
await webdav_su.deleteObject(resource)
|
||||
})
|
||||
|
||||
// DAVCalendarAclTest
|
||||
|
||||
it("'view all' on a specific class (PUBLIC)", async function() {
|
||||
await _testRights({ pu: 'v' })
|
||||
})
|
||||
|
||||
it("'modify' PUBLIC, 'view all' PRIVATE, 'view d&t' confidential", async function() {
|
||||
await _testRights({ pu: 'm', pr: 'v', co: 'd' })
|
||||
})
|
||||
|
||||
it("'create' only", async function() {
|
||||
await _testRights({ c: true })
|
||||
})
|
||||
|
||||
it("'delete' only", async function() {
|
||||
await _testRights({ d: true })
|
||||
})
|
||||
|
||||
it("'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE", async function() {
|
||||
await _testRights({ c: true, d: true, pu: 'd', pr: 'm' })
|
||||
})
|
||||
|
||||
it("'create', 'respond to' PUBLIC", async function() {
|
||||
await _testRights({ c: true, pu: 'r' })
|
||||
})
|
||||
|
||||
it("no right given", async function() {
|
||||
await _testRights({})
|
||||
})
|
||||
|
||||
})
|
||||
229
Tests/spec/DAVCalendarAppleiCalSpec.js
Normal file
229
Tests/spec/DAVCalendarAppleiCalSpec.js
Normal file
@@ -0,0 +1,229 @@
|
||||
import { DAVNamespace } from 'tsdav'
|
||||
import config from '../lib/config'
|
||||
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
/**
|
||||
* NOTE
|
||||
*
|
||||
* To pass the following tests, make sure "username" and "subscriber_username" don't have
|
||||
* additional calendars.
|
||||
*/
|
||||
|
||||
describe('Apple iCal', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const utility = new TestUtility(webdav_su)
|
||||
|
||||
const iCal4UserAgent = 'DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)'
|
||||
|
||||
const _setMemberSet = async function(owner, members, perm) {
|
||||
const resource = `/SOGo/dav/${owner}/calendar-proxy-${perm}`
|
||||
const headers = { 'User-Agent': iCal4UserAgent }
|
||||
const membersHref = members.map(m => {
|
||||
return `/SOGo/dav/${m}`
|
||||
})
|
||||
const properties = {
|
||||
'group-member-set': membersHref.length ? { href: membersHref } : ''
|
||||
}
|
||||
const results = await webdav_su.proppatchWebdav(resource, properties, DAVNamespace.DAV, headers)
|
||||
|
||||
expect(results.length)
|
||||
.withContext(`Number of responses from PROPPATCH on group-member-set for ${owner}`)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`HTTP status code when setting group member on calendar-proxy-${perm} for ${owner}`)
|
||||
.toBe(207)
|
||||
}
|
||||
|
||||
const _getMembership = async function(user) {
|
||||
const resource = `/SOGo/dav/${user}/`
|
||||
const headers = { 'User-Agent': iCal4UserAgent }
|
||||
const results = await webdav_su.propfindWebdav(resource, ['group-membership'], DAVNamespace.DAV, headers)
|
||||
|
||||
expect(results.length)
|
||||
.withContext(`Number of responses from PROPFIND on group-membership for ${user}`)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`HTTP status code when getting group membership for ${user}`)
|
||||
.toBe(207)
|
||||
|
||||
const { props: { groupMembership: { href = [] } = {} } = {} } = results[0]
|
||||
|
||||
return Array.isArray(href) ? href : [href] // always return an array
|
||||
}
|
||||
|
||||
const _getProxyFor = async function(user, perm) {
|
||||
const resource = `/SOGo/dav/${user}/`
|
||||
const headers = { 'User-Agent': iCal4UserAgent }
|
||||
const results = await webdav_su.propfindWebdav(resource, [`calendar-proxy-${perm}-for`], DAVNamespace.CALENDAR_SERVER, headers)
|
||||
|
||||
expect(results.length)
|
||||
.withContext(`Number of responses from PROPFIND on group-membership for ${user}`)
|
||||
.toBe(1)
|
||||
expect(results[0].status)
|
||||
.withContext(`HTTP status code when getting group membership for ${user}`)
|
||||
.toBe(207)
|
||||
|
||||
const { props = {} } = results[0]
|
||||
const users = props[`calendarProxy${perm.replace(/^\w/, (c) => c.toUpperCase())}For`]
|
||||
const { href = [] } = users
|
||||
|
||||
return Array.isArray(href) ? href : [href] // always return an array
|
||||
}
|
||||
|
||||
const _testMapping = async function(perm, resource, rights) {
|
||||
const results = await utility.setupCalendarRights(resource, config.subscriber_username, rights)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].status).toBe(204)
|
||||
|
||||
const membership = await _getMembership(config.subscriber_username)
|
||||
expect(membership)
|
||||
.withContext(`${perm.replace(/^\w/, (c) => c.toUpperCase())} access to /SOGo/dav/${config.subscriber_username}/`)
|
||||
.toContain(`/SOGo/dav/${config.username}/calendar-proxy-${perm}/`)
|
||||
|
||||
const proxyFor = await _getProxyFor(config.subscriber_username, perm)
|
||||
expect(proxyFor)
|
||||
.withContext(`Proxy ${perm} on /SOGo/dav/${config.subscriber_username}/`)
|
||||
.toContain(`/SOGo/dav/${config.username}/`)
|
||||
}
|
||||
|
||||
// iCalTest
|
||||
|
||||
it(`principal-collection-set: 'DAV' header must be returned with iCal 4`, async function() {
|
||||
const resource = `/SOGo/dav/${config.username}/`
|
||||
const expectedDAVClasses = ['1', '2', 'access-control', 'calendar-access', 'calendar-schedule', 'calendar-auto-schedule', 'calendar-proxy']
|
||||
|
||||
let headers, response, davClasses, davClass
|
||||
headers = { Depth: new String(0) }
|
||||
|
||||
// NOT iCal4
|
||||
response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers)
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status code when fetching principal-collection-set`)
|
||||
.toBe(207)
|
||||
expect(response.headers.get('dav'))
|
||||
.withContext(`DAV header must NOT be returned when user-agent is NOT iCal 4`)
|
||||
.toBeFalsy()
|
||||
|
||||
// iCal4
|
||||
headers['User-Agent'] = iCal4UserAgent
|
||||
response = await webdav.propfindWebdavRaw(resource, ['principal-collection-set'], headers)
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status code when fetching principal-collection-set`)
|
||||
.toBe(207)
|
||||
expect(response.headers.get('dav'))
|
||||
.withContext(`DAV header must be returned when user-agent is iCal 4`)
|
||||
.toBeTruthy()
|
||||
|
||||
davClasses = response.headers.get('dav').split(', ')
|
||||
for (davClass of expectedDAVClasses) {
|
||||
expect(davClasses.includes(davClass))
|
||||
.withContext(`DAV header includes class ${davClass}`)
|
||||
.toBeTrue()
|
||||
}
|
||||
})
|
||||
|
||||
it(`calendar-proxy as used from iCal`, async function() {
|
||||
let membership, perm, users, proxyFor
|
||||
|
||||
await _setMemberSet(config.username, [], 'read')
|
||||
await _setMemberSet(config.username, [], 'write')
|
||||
await _setMemberSet(config.subscriber_username, [], 'read')
|
||||
await _setMemberSet(config.subscriber_username, [], 'write')
|
||||
|
||||
membership = await _getMembership(config.username)
|
||||
expect(membership.length)
|
||||
.toBe(0)
|
||||
membership = await _getMembership(config.subscriber_username)
|
||||
expect(membership.length)
|
||||
.toBe(0)
|
||||
|
||||
users = await _getProxyFor(config.username, 'read')
|
||||
expect(users.length)
|
||||
.withContext(`Proxy read for /SOGo/dav/${config.username}`)
|
||||
.toBe(0)
|
||||
users = await _getProxyFor(config.username, 'write')
|
||||
expect(users.length)
|
||||
.withContext(`Proxy write for /SOGo/dav/${config.username}`)
|
||||
.toBe(0)
|
||||
users = await _getProxyFor(config.subscriber_username, 'read')
|
||||
expect(users.length)
|
||||
.withContext(`Proxy read for /SOGo/dav/${config.subscriber_username}`)
|
||||
.toBe(0)
|
||||
users = await _getProxyFor(config.subscriber_username, 'write')
|
||||
expect(users.length)
|
||||
.withContext(`Proxy write for /SOGo/dav/${config.subscriber_username}`)
|
||||
.toBe(0)
|
||||
|
||||
for (perm of ['read', 'write']) {
|
||||
for (users of [[config.username, config.subscriber_username], [config.subscriber_username, config.username]]) {
|
||||
const [owner, member] = users
|
||||
|
||||
await _setMemberSet(owner, [member], perm)
|
||||
|
||||
let [ membership ] = await _getMembership(member)
|
||||
expect(membership)
|
||||
.toBe(`/SOGo/dav/${owner}/calendar-proxy-${perm}/`)
|
||||
|
||||
proxyFor = await _getProxyFor(member, perm)
|
||||
expect(proxyFor.length).toBe(1)
|
||||
expect(proxyFor).toContain(`/SOGo/dav/${owner}/`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('calendar-proxy as used from SOGo', async function() {
|
||||
const personalResource = `/SOGo/dav/${config.username}/Calendar/personal/`
|
||||
const otherResource = `/SOGo/dav/${config.username}/Calendar/test-calendar-proxy2/`
|
||||
|
||||
let response, membership
|
||||
|
||||
// Remove rights on personal calendar
|
||||
await utility.setupRights(personalResource, config.subscriber_username);
|
||||
[response] = await utility.subscribe(personalResource, [config.subscriber_username])
|
||||
expect(response.status)
|
||||
.toBe(200)
|
||||
|
||||
await webdav_su.deleteObject(otherResource)
|
||||
await webdav_su.makeCalendar(otherResource)
|
||||
await utility.setupRights(otherResource, config.subscriber_username);
|
||||
[response] = await utility.subscribe(otherResource, [config.subscriber_username])
|
||||
expect(response.status)
|
||||
.toBe(200)
|
||||
|
||||
// we test the rights mapping
|
||||
// write: write on 'personal', none on 'test-calendar-proxy2'
|
||||
await _testMapping('write', personalResource, { c: true, d: false, pu: 'v' })
|
||||
await _testMapping('write', personalResource, { c: false, d: true, pu: 'v' })
|
||||
await _testMapping('write', personalResource, { c: false, d: false, pu: 'm' })
|
||||
await _testMapping('write', personalResource, { c: false, d: false, pu: 'r' })
|
||||
|
||||
// read: read on 'personal', none on 'test-calendar-proxy2'
|
||||
await _testMapping('read', personalResource, { c: false, d: false, pu: 'd' })
|
||||
await _testMapping('read', personalResource, { c: false, d: false, pu: 'v' })
|
||||
|
||||
// write: read on 'personal', write on 'test-calendar-proxy2'
|
||||
await _testMapping('write', otherResource, { c: false, d: false, pu: 'r' });
|
||||
|
||||
// we test the unsubscription
|
||||
// unsubscribed from personal, subscribed to 'test-calendar-proxy2'
|
||||
[response] = await utility.unsubscribe(personalResource, [config.subscriber_username])
|
||||
expect(response.status)
|
||||
.toBe(200)
|
||||
membership = await _getMembership(config.subscriber_username)
|
||||
expect(membership)
|
||||
.withContext(`Proxy write to /SOGo/dav/${config.subscriber_username}/`)
|
||||
.toContain(`/SOGo/dav/${config.username}/calendar-proxy-write/`);
|
||||
// unsubscribed from personal, unsubscribed from 'test-calendar-proxy2'
|
||||
[response] = await utility.unsubscribe(otherResource, [config.subscriber_username])
|
||||
expect(response.status)
|
||||
.toBe(200)
|
||||
membership = await _getMembership(config.subscriber_username)
|
||||
expect(membership.length)
|
||||
.withContext(`No more access to /SOGo/dav/${config.subscriber_username}/`)
|
||||
.toBe(0)
|
||||
|
||||
await webdav_su.deleteObject(otherResource)
|
||||
})
|
||||
})
|
||||
50
Tests/spec/DAVCalendarClassificationSpec.js
Normal file
50
Tests/spec/DAVCalendarClassificationSpec.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import config from '../lib/config'
|
||||
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
|
||||
|
||||
describe('calendar classification', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
|
||||
const _setClassification = async function(component, classification = '') {
|
||||
const resource = `/SOGo/dav/${config.username}/Calendar/`
|
||||
const properties = { [`${component}-default-classification`]: classification }
|
||||
|
||||
const results = await webdav.proppatchWebdav(resource, properties, DAVInverse)
|
||||
expect(results.length)
|
||||
.withContext(`Set ${component} classification to ${classification}`)
|
||||
.toBe(1)
|
||||
|
||||
return results[0].status
|
||||
}
|
||||
|
||||
// HTTPDefaultClassificationTest
|
||||
|
||||
it('expected failure when setting a classification with an invalid property', async function() {
|
||||
let status
|
||||
|
||||
status = await _setClassification('123456', 'PUBLIC')
|
||||
expect(status)
|
||||
.withContext('Setting an invalid classification property')
|
||||
.toBe(403)
|
||||
|
||||
status = await _setClassification('events', '')
|
||||
expect(status)
|
||||
.withContext('Setting an empty classification')
|
||||
.toBe(403)
|
||||
|
||||
status = await _setClassification('events', 'pouet')
|
||||
expect(status)
|
||||
.withContext('Setting an invalid classification')
|
||||
.toBe(403)
|
||||
})
|
||||
|
||||
it('setting a valid classification', async function() {
|
||||
for (let component of ['events', 'tasks']) {
|
||||
for (let classification of ['PUBLIC', 'PRIVATE', 'CONFIDENTIAL']) {
|
||||
const status = await _setClassification(component, classification)
|
||||
expect(status)
|
||||
.withContext(`Set ${component} classification to ${classification}`)
|
||||
.toBe(207)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
181
Tests/spec/DAVCalendarPublicAclSpec.js
Normal file
181
Tests/spec/DAVCalendarPublicAclSpec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
describe('public access', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_anon = new WebDAV()
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const webdav_subscriber = new WebDAV(config.subscriber_username, config.subscriber_password)
|
||||
const utility = new TestUtility(webdav)
|
||||
const utility_subscriber = new TestUtility(webdav_subscriber)
|
||||
let createdRsrc
|
||||
|
||||
// DAVCalendarPublicAclTest
|
||||
|
||||
afterEach(async function() {
|
||||
if (createdRsrc) {
|
||||
await webdav_su.deleteObject(createdRsrc)
|
||||
}
|
||||
})
|
||||
|
||||
it("normal user access to (non-)shared resource from su", async function() {
|
||||
const parentColl = `/SOGo/dav/${config.username}/Calendar/`
|
||||
let results
|
||||
let href
|
||||
|
||||
// 1. all rights removed
|
||||
createdRsrc = `${parentColl}test-dav-acl/`
|
||||
for (const rsrc of ['personal', 'test-dav-acl']) {
|
||||
const resource = `${parentColl}${rsrc}/`
|
||||
await webdav.makeCalendar(resource)
|
||||
await utility.setupRights(resource, 'anonymous', {})
|
||||
await utility.setupRights(resource, config.subscriber_username, {})
|
||||
await utility.setupRights(resource, '<default>', {})
|
||||
}
|
||||
|
||||
results = await webdav_subscriber.propfindURL(parentColl)
|
||||
expect(results.length)
|
||||
.withContext(`Profind returns 1 href when subscriber user ${config.subscriber_username} has no right`)
|
||||
.toBe(1)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext(`Unique href must be the Calendar parent collection ${parentColl}`)
|
||||
.toBe(parentColl)
|
||||
|
||||
// 2. creation right added
|
||||
await utility.setupCalendarRights(createdRsrc, config.subscriber_username, { c: true })
|
||||
|
||||
results = await webdav_subscriber.propfindURL(parentColl)
|
||||
expect(results.length)
|
||||
.withContext(`Profind returns 4 href when subscriber user ${config.subscriber_username} has creation right`)
|
||||
.toBe(4)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext(`First href must be the Calendar parent collection ${parentColl}`)
|
||||
.toBe(parentColl)
|
||||
|
||||
let resourceHrefs = {
|
||||
[createdRsrc]: false,
|
||||
[`${createdRsrc.slice(0, -1)}.xml`]: false,
|
||||
[`${createdRsrc.slice(0, -1)}.ics`]: false
|
||||
}
|
||||
for (href of results.map(r => r.href).slice(1)) {
|
||||
expect(Object.keys(resourceHrefs).includes(href))
|
||||
.withContext(`Propfind href ${href} is returned`)
|
||||
.toBeTrue()
|
||||
expect(resourceHrefs[href])
|
||||
.not.toBeTrue()
|
||||
resourceHrefs[href] = true
|
||||
}
|
||||
|
||||
await utility.setupRights(createdRsrc, config.subscriber_username) // remove rights
|
||||
|
||||
// 3. creation right added for "default user"
|
||||
// subscriber_username expected to have access, but not "anonymous"
|
||||
await utility.setupCalendarRights(createdRsrc, '<default>', { c: true })
|
||||
|
||||
results = await webdav_subscriber.propfindURL(parentColl)
|
||||
expect(results.length)
|
||||
.withContext('Profind returns 4 href when <default> user has creation right')
|
||||
.toBe(4)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext('First href must be the Calendar parent collection')
|
||||
.toBe(parentColl)
|
||||
|
||||
resourceHrefs = {
|
||||
[createdRsrc]: false,
|
||||
[`${createdRsrc.slice(0, -1)}.xml`]: false,
|
||||
[`${createdRsrc.slice(0, -1)}.ics`]: false
|
||||
}
|
||||
for (href of results.map(r => r.href).slice(1)) {
|
||||
expect(Object.keys(resourceHrefs).includes(href))
|
||||
.withContext(`Propfind href ${href} is returned`)
|
||||
.toBeTrue()
|
||||
expect(resourceHrefs[href])
|
||||
.withContext(`Propfind href ${href} is returned only once`)
|
||||
.not.toBeTrue()
|
||||
resourceHrefs[href] = true
|
||||
}
|
||||
|
||||
const anonParentColl = `/SOGo/dav/public/${config.username}/Calendar/`
|
||||
results = await webdav_anon.propfindURL(anonParentColl)
|
||||
expect(results.length)
|
||||
.withContext('Profind returns 1 href for anonymous user')
|
||||
.toBe(1)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext('Unique href must be the Calendar parent collection')
|
||||
.toBe(anonParentColl)
|
||||
|
||||
await utility.setupRights(createdRsrc, '<default>', {})
|
||||
|
||||
// 4. creation right added for "anonymous"
|
||||
// "anonymous" expected to have access, but not subscriber_username
|
||||
|
||||
await utility.setupCalendarRights(createdRsrc, 'anonymous', { c: true })
|
||||
|
||||
results = await webdav_anon.propfindURL(anonParentColl)
|
||||
expect(results.length)
|
||||
.withContext('Profind returns 4 href when anonymous user has creation right')
|
||||
.toBe(4)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext('First href must be the Calendar parent collection')
|
||||
.toBe(anonParentColl)
|
||||
|
||||
const anonRsrc = `${anonParentColl}test-dav-acl/`
|
||||
resourceHrefs = {
|
||||
[anonRsrc]: false,
|
||||
[`${anonRsrc.slice(0, -1)}.xml`]: false,
|
||||
[`${anonRsrc.slice(0, -1)}.ics`]: false
|
||||
}
|
||||
for (href of results.map(r => r.href).slice(1)) {
|
||||
expect(Object.keys(resourceHrefs).includes(href))
|
||||
.withContext(`Propfind href ${href} is returned`)
|
||||
.toBeTrue()
|
||||
expect(resourceHrefs[href])
|
||||
.withContext(`Propfind href ${href} is returned only once`)
|
||||
.not.toBeTrue()
|
||||
resourceHrefs[href] = true
|
||||
}
|
||||
|
||||
results = await webdav_subscriber.propfindURL(parentColl)
|
||||
expect(results.length)
|
||||
.withContext('Profind returns 1 href when <default> user has no right')
|
||||
.toBe(1)
|
||||
href = results[0].href
|
||||
expect(href)
|
||||
.withContext('First href must be the Calendar parent collection')
|
||||
.toBe(parentColl)
|
||||
|
||||
})
|
||||
|
||||
it("user accessing (non-)shared Calendars", async function() {
|
||||
const parentColl = `/SOGo/dav/${config.subscriber_username}/Calendar/`
|
||||
let results
|
||||
|
||||
createdRsrc = `${parentColl}test-dav-acl/`
|
||||
for (const rsrc of ['personal', 'test-dav-acl']) {
|
||||
const resource = `${parentColl}${rsrc}/`
|
||||
await webdav_su.makeCalendar(resource)
|
||||
await utility_subscriber.setupRights(resource, config.username, {})
|
||||
}
|
||||
|
||||
results = await webdav_subscriber.propfindURL(parentColl)
|
||||
const hrefs = results.map(r => r.href).filter(h => {
|
||||
return h == `${parentColl}` ||
|
||||
h.indexOf(`${parentColl}personal`) == 0 ||
|
||||
h.indexOf(`${parentColl}test-dav-acl`) == 0
|
||||
})
|
||||
expect(hrefs.length)
|
||||
.withContext(`Profind returns at least 3 hrefs when user ${config.subscriber_username} is the owner`)
|
||||
.toBeGreaterThan(2)
|
||||
const [href] = hrefs
|
||||
expect(href)
|
||||
.withContext('Unique href must be the Calendar parent collection')
|
||||
.toBe(parentColl)
|
||||
})
|
||||
|
||||
})
|
||||
116
Tests/spec/DAVCalendarSuperUserAclSpec.js
Normal file
116
Tests/spec/DAVCalendarSuperUserAclSpec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
describe('DAVCalendarSuperUserAcl', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
const utility = new TestUtility(webdav)
|
||||
|
||||
const event_template = `BEGIN:VCALENDAR
|
||||
PRODID:-//Inverse//Event Generator//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
SEQUENCE:0
|
||||
TRANSP:OPAQUE
|
||||
UID:12345-%(class)-%(filename)
|
||||
SUMMARY:%(class) event (orig. title)
|
||||
DTSTART:20090805T100000Z
|
||||
DTEND:20090805T140000Z
|
||||
CLASS:%(class)
|
||||
DESCRIPTION:%(class) description
|
||||
LOCATION:location
|
||||
%(organizer_line)%(attendee_line)CREATED:20090805T100000Z
|
||||
DTSTAMP:20090805T100000Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`
|
||||
|
||||
const resource = `/SOGo/dav/${config.subscriber_username}/Calendar/test-dav-superuser-acl/`
|
||||
const filename = 'suevent.ics'
|
||||
|
||||
const event = utility.formatTemplate(event_template, {
|
||||
'class': 'PUBLIC',
|
||||
'filename': filename
|
||||
})
|
||||
|
||||
beforeAll(async function() {
|
||||
await webdav_su.deleteObject(resource)
|
||||
await webdav_su.makeCalendar(resource)
|
||||
})
|
||||
|
||||
afterAll(async function() {
|
||||
await webdav_su.deleteObject(resource)
|
||||
})
|
||||
|
||||
// DAVCalendarSuperUserAclTest.testSUAccess
|
||||
it("create, read, modify, delete for superuser", async function() {
|
||||
let result, results
|
||||
|
||||
// 1. Create
|
||||
|
||||
result = await webdav_su.createCalendarObject(resource, filename, event)
|
||||
expect(result.status)
|
||||
.withContext('Event creation returns status code 201')
|
||||
.toBe(201)
|
||||
|
||||
// 2. Read - GET
|
||||
|
||||
results = await webdav_su.getObject(resource, filename)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results[0].raw.replace(/\r\n/g,'\n')).toBe(event)
|
||||
|
||||
// 2. Read - PROPFIND calendar-data
|
||||
|
||||
results = await webdav_su.propfindEvent(resource + filename)
|
||||
expect(results.length).toBe(2) // suevent.ics + suevent.ics/master
|
||||
expect(results.find(o => {
|
||||
if (o.href == resource + filename) {
|
||||
expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})).toBeTruthy()
|
||||
|
||||
// 2. Read - REPORT calendar-multiget
|
||||
|
||||
results = await webdav_su.calendarMultiGet(resource, filename)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results.find(o => {
|
||||
if (o.href == resource + filename) {
|
||||
expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})).toBeTruthy()
|
||||
|
||||
// 2. Read - webdav-sync
|
||||
|
||||
results = await webdav_su.syncCollection(resource)
|
||||
expect(results.length).toBe(1)
|
||||
expect(results.find(o => {
|
||||
expect(o.status).toBe(207)
|
||||
if (o.href == resource + filename) {
|
||||
expect(o.props.calendarData.replace(/\r\n/g,'\n')).toBe(event)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})).toBeTruthy()
|
||||
|
||||
// 3. Modify
|
||||
|
||||
const classes = ['CONFIDENTIAL', 'PRIVATE', 'PUBLIC']
|
||||
for (const c of classes) {
|
||||
const event = utility.formatTemplate(event_template, {
|
||||
'class': c,
|
||||
'filename': filename
|
||||
})
|
||||
const response = await webdav_su.createCalendarObject(resource, filename, event)
|
||||
expect(response.status).toBe(204)
|
||||
}
|
||||
|
||||
// 4. Delete
|
||||
const response = await webdav_su.deleteObject(resource)
|
||||
expect(response.status).toBe(204)
|
||||
})
|
||||
|
||||
})
|
||||
68
Tests/spec/DAVContactsCategoriesSpec.js
Normal file
68
Tests/spec/DAVContactsCategoriesSpec.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import config from '../lib/config'
|
||||
import { default as WebDAV, DAVInverse } from '../lib/WebDAV'
|
||||
|
||||
describe('contacts categories', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
|
||||
const _setCategories = async function(categories = []) {
|
||||
const resource = `/SOGo/dav/${config.username}/Contacts/`
|
||||
const elements = categories.map(c => {
|
||||
return { 'category': c }
|
||||
})
|
||||
const properties = { 'contacts-categories': elements.length ? elements : '' }
|
||||
|
||||
const results = await webdav.proppatchWebdav(resource, properties, DAVInverse)
|
||||
expect(results.length)
|
||||
.withContext(`Set contacts categories to ${categories.join(', ')}`)
|
||||
.toBe(1)
|
||||
|
||||
return results[0].status
|
||||
}
|
||||
|
||||
const _getCategories = async function() {
|
||||
const resource = `/SOGo/dav/${config.username}/Contacts/`
|
||||
const properties = ['contacts-categories']
|
||||
|
||||
const results = await webdav.propfindWebdav(resource, properties, DAVInverse)
|
||||
expect(results.length)
|
||||
.toBe(1)
|
||||
const { props: { contactsCategories: { category } = {} } = {} } = results[0]
|
||||
|
||||
return category
|
||||
}
|
||||
|
||||
// HTTPContactCategoriesTest
|
||||
|
||||
it('setting contacts categories', async function() {
|
||||
let status, results
|
||||
|
||||
status = await _setCategories()
|
||||
expect(status)
|
||||
.withContext('Removing contacts categories')
|
||||
.toBe(207)
|
||||
results = await _getCategories()
|
||||
expect(results)
|
||||
.toBeUndefined()
|
||||
|
||||
status = await _setCategories(['Coucou'])
|
||||
expect(status)
|
||||
.withContext('Setting one contacts category')
|
||||
.toBe(207)
|
||||
results = await _getCategories()
|
||||
expect(results)
|
||||
.toBe('Coucou')
|
||||
|
||||
status = await _setCategories(['Toto', 'Cuicui'])
|
||||
expect(status)
|
||||
.withContext('Setting two contacts category')
|
||||
.toBe(207)
|
||||
|
||||
results = await _getCategories()
|
||||
expect(results.length)
|
||||
.toBe(2)
|
||||
expect(results)
|
||||
.toContain('Toto')
|
||||
expect(results)
|
||||
.toContain('Cuicui')
|
||||
})
|
||||
})
|
||||
39
Tests/spec/DAVPublicAccessSpec.js
Normal file
39
Tests/spec/DAVPublicAccessSpec.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
|
||||
describe('public access', function() {
|
||||
const webdav_anon = new WebDAV()
|
||||
|
||||
// DAVPublicAccessTest
|
||||
|
||||
it("access to /SOGo/so/public", async function() {
|
||||
const [{ status }] = await webdav_anon.options('/SOGo/so/public')
|
||||
expect(status)
|
||||
.withContext('/SOGo/so/public must not be accessible')
|
||||
.toBe(404)
|
||||
})
|
||||
|
||||
it("access to /SOGo/public", async function() {
|
||||
const [{ status }] = await webdav_anon.options('/SOGo/public')
|
||||
expect(status)
|
||||
.withContext('/SOGo/public must not be accessible')
|
||||
.toBe(404)
|
||||
})
|
||||
|
||||
it("access to non-public resource", async function() {
|
||||
const [{ status }] = await webdav_anon.options(`/SOGo/dav/${config.username}`)
|
||||
expect(status)
|
||||
.withContext('DAV non-public resources should request authentication')
|
||||
.toBe(401)
|
||||
})
|
||||
|
||||
it("access to public resource", async function() {
|
||||
const [{ status }] = await webdav_anon.options('/SOGo/dav/public')
|
||||
expect(status)
|
||||
.withContext('DAV public resources must not request authentication')
|
||||
.not.toBe(401)
|
||||
expect(status)
|
||||
.withContext('DAV public resources must be accessible')
|
||||
.toBe(200)
|
||||
})
|
||||
})
|
||||
35
Tests/spec/SogoToolSpec.js
Normal file
35
Tests/spec/SogoToolSpec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import config from '../lib/config'
|
||||
import { mkdtempSync, rmSync } from 'fs'
|
||||
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
describe('sogo-tool tests', function() {
|
||||
let tmpdir, isRoot
|
||||
|
||||
beforeAll(function() {
|
||||
const { uid } = os.userInfo()
|
||||
isRoot = (uid == 0)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
tmpdir = mkdtempSync(path.join(os.tmpdir(), 'sogo-'))
|
||||
if (isRoot) {
|
||||
execSync(`chown -R sogo:sogo ${tmpdir}`)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
rmSync(tmpdir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('backup', async function() {
|
||||
const sudo = isRoot ? `sudo -u sogo ` : ``
|
||||
try {
|
||||
execSync(`${sudo}sogo-tool backup ${tmpdir} ${config.username} 2>&1`)
|
||||
} catch (err) {
|
||||
fail(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
120
Tests/spec/WebDAVSpec.js
Normal file
120
Tests/spec/WebDAVSpec.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import TestUtility from '../lib/utilities'
|
||||
|
||||
describe('WebDAV', function() {
|
||||
var webdav
|
||||
var utility
|
||||
|
||||
beforeEach(function() {
|
||||
webdav = new WebDAV(config.username, config.password)
|
||||
utility = new TestUtility(webdav)
|
||||
})
|
||||
|
||||
it("property: 'principal-collection-set' on collection object", async function() {
|
||||
const resource = `/SOGo/dav/${config.username}/`
|
||||
const results = await webdav.principalCollectionSet(resource)
|
||||
expect(results.length).toBe(1)
|
||||
results.forEach(o => {
|
||||
expect(o.ok).toBe(true)
|
||||
expect(o.status).toBe(207)
|
||||
expect(resource).toBe(o.href)
|
||||
})
|
||||
})
|
||||
|
||||
it("property: 'principal-collection-set' on non-collection object", function() {
|
||||
const resource = `/SOGo/dav/${config.username}/freebusy.ifb`
|
||||
return webdav.principalCollectionSet(resource).then(function(results) {
|
||||
expect(results.length).toBe(1)
|
||||
results.forEach(o => {
|
||||
expect(o.ok).toBe(true)
|
||||
expect(o.status).toBe(207)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("propfind: ensure various NSURL work-arounds", async function() {
|
||||
const resultsNoSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}`)
|
||||
resultsNoSlash.forEach(o => {
|
||||
// Expect no trailing slash nowhere
|
||||
expect(o.href.slice(-1)).toMatch(/[^\/]$/)
|
||||
})
|
||||
const resultsWithSlash = await webdav.propfindURL(`/SOGo/dav/${config.username}/`)
|
||||
resultsWithSlash.forEach(o => {
|
||||
// Expect a trailing slash for collections only
|
||||
if (o.props.resourcetype.collection) {
|
||||
expect(o.href.slice(-1)).toMatch(/\/$/)
|
||||
}
|
||||
else {
|
||||
expect(o.href.slice(-1)).toMatch(/[^\/]$/)
|
||||
}
|
||||
})
|
||||
const resultsNoColl = await webdav.propfindURL(`/SOGo/dav/${config.username}/freebusy.ifb`)
|
||||
resultsNoColl.forEach(o => {
|
||||
// Expect no collection
|
||||
expect(o.props.resourcetype.collection).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
// REPORT
|
||||
it("principal-property-search", async function() {
|
||||
const resource = `/SOGo/dav/${config.username}/Calendar`
|
||||
const user = await utility.fetchUserInfo(config.username)
|
||||
const results = await webdav.principalPropertySearch(resource)
|
||||
expect(results.length).toBe(1)
|
||||
results.forEach(o => {
|
||||
expect(o.status)
|
||||
.withContext(`HTTP status code when a performing a property search`)
|
||||
.toBe(207)
|
||||
expect(o.href).toBe(`/SOGo/dav/${config.username}/`)
|
||||
expect(o.props.displayname).toBe(user.displayname)
|
||||
})
|
||||
})
|
||||
|
||||
// http://tools.ietf.org/html/rfc3253.html#section-3.8
|
||||
it("expand-property", async function () {
|
||||
const resource = `/SOGo/dav/${config.username}/`
|
||||
const user = await utility.fetchUserInfo(config.username)
|
||||
const properties = [
|
||||
{
|
||||
_attributes: {
|
||||
name: 'owner'
|
||||
},
|
||||
property: { _attributes: { name: 'displayname' } }
|
||||
},
|
||||
{
|
||||
_attributes: {
|
||||
name: 'principal-collection-set'
|
||||
},
|
||||
property: { _attributes: { name: 'displayname' } }
|
||||
}
|
||||
]
|
||||
const outcomes = {
|
||||
owner: {
|
||||
href: resource,
|
||||
displayname: user.displayname
|
||||
},
|
||||
principalCollectionSet: {
|
||||
href: '/SOGo/dav/',
|
||||
displayname: 'SOGo'
|
||||
}
|
||||
}
|
||||
const results = await webdav.expendProperty(resource, properties)
|
||||
expect(results.length).toBe(1)
|
||||
results.forEach(o => {
|
||||
const { props = {} } = o
|
||||
expect(o.status)
|
||||
.withContext(`HTTP status code when expanding properties`)
|
||||
.toBe(207)
|
||||
Object.keys(outcomes).forEach(p => {
|
||||
const { response: { href, propstat: { prop: { displayname }} }} = props[p]
|
||||
expect(href)
|
||||
.withContext(`Result of expand-property for href`)
|
||||
.toBe(outcomes[p].href)
|
||||
expect(displayname)
|
||||
.withContext(`Result of expand-property for displayname`)
|
||||
.toBe(outcomes[p].displayname)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
61
Tests/spec/WebDavSyncSpec.js
Normal file
61
Tests/spec/WebDavSyncSpec.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import config from '../lib/config'
|
||||
import WebDAV from '../lib/WebDAV'
|
||||
import { DAVNamespace, DAVNamespaceShorthandMap } from 'tsdav'
|
||||
import convert from 'xml-js'
|
||||
|
||||
describe('webdav sync', function() {
|
||||
const webdav = new WebDAV(config.username, config.password)
|
||||
const webdav_su = new WebDAV(config.superuser, config.superuser_password)
|
||||
// const resource = `/SOGo/dav/${config.username}/Calendar/test-webdavsync/`
|
||||
const resource = `/SOGo/dav/${config.username}/Calendar/personal/`
|
||||
|
||||
afterEach(async function() {
|
||||
// await webdav_su.deleteObject(resource)
|
||||
})
|
||||
|
||||
it('webdav sync', async function() {
|
||||
const nsShort = DAVNamespaceShorthandMap[DAVNamespace.DAV].toUpperCase()
|
||||
let response, xml, token
|
||||
|
||||
// missing tests:
|
||||
// invalid tokens: negative, non-numeric, > current timestamp
|
||||
// non-empty collections: token validity, status codes for added,
|
||||
// modified and removed elements
|
||||
|
||||
// response = await webdav.makeCalendar(resource)
|
||||
// expect(response.length).toBe(1)
|
||||
// expect(response[0].status)
|
||||
// .withContext(`HTTP status code when creating a Calendar`)
|
||||
// .toBe(201)
|
||||
|
||||
// test queries:
|
||||
// empty collection:
|
||||
// without a token (query1)
|
||||
// with a token (query2)
|
||||
// (when done, non-empty collection:
|
||||
// without a token (query3)
|
||||
// with a token (query4))
|
||||
|
||||
response = await webdav.syncCollectionRaw(resource, null, [ 'getetag' ])
|
||||
xml = await response.text();
|
||||
({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true}))
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status code when performing sync-query without a token`)
|
||||
.toBe(207)
|
||||
expect(token)
|
||||
.withContext(`Sync query returns valid token`)
|
||||
.toBeGreaterThanOrEqual(0)
|
||||
|
||||
// we make sure that any token is accepted when the collection is
|
||||
// empty, but that the returned token differs
|
||||
response = await webdav.syncCollectionRaw(resource, '1234', [ 'getetag' ])
|
||||
xml = await response.text();
|
||||
({ [`${nsShort}:multistatus`]: { [`${nsShort}:sync-token`]: { _text: token } } } = convert.xml2js(xml, {compact: true, nativeType: true}))
|
||||
expect(response.status)
|
||||
.withContext(`HTTP status code when performing sync-query with a token`)
|
||||
.toBe(207)
|
||||
expect(token)
|
||||
.withContext(`Sync query returns valid token`)
|
||||
.toBeGreaterThanOrEqual(0)
|
||||
})
|
||||
})
|
||||
14
Tests/spec/support/jasmine.json
Normal file
14
Tests/spec/support/jasmine.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": [
|
||||
"**/*[sS]pec.?(m)js"
|
||||
],
|
||||
"helpers": [
|
||||
"helpers/**/*.?(m)js"
|
||||
],
|
||||
"requires": [
|
||||
"esm"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": true
|
||||
}
|
||||
@@ -299,7 +299,7 @@ _xmlCharsetForCharset (NSString *charset)
|
||||
attributes: (id <SaxAttributes>) _attributes
|
||||
{
|
||||
unsigned int count, max;
|
||||
NSString *name, *value, *cid, *lowerName;
|
||||
NSString *name, *value, *cid, *lowerName, *lowerValue;
|
||||
NSMutableString *resultPart;
|
||||
BOOL skipAttribute;
|
||||
|
||||
@@ -340,7 +340,8 @@ _xmlCharsetForCharset (NSString *charset)
|
||||
{
|
||||
skipAttribute = NO;
|
||||
name = [[_attributes nameAtIndex: count] lowercaseString];
|
||||
if ([name hasPrefix: @"ON"])
|
||||
if ([name hasPrefix: @"on"])
|
||||
// on Events
|
||||
skipAttribute = YES;
|
||||
else if ([name isEqualToString: @"src"])
|
||||
{
|
||||
@@ -370,13 +371,19 @@ _xmlCharsetForCharset (NSString *charset)
|
||||
name = [NSString stringWithFormat: @"unsafe-%@", name];
|
||||
}
|
||||
else if ([name isEqualToString: @"href"]
|
||||
|| [name isEqualToString: @"action"])
|
||||
|| [name isEqualToString: @"action"]
|
||||
|| [name isEqualToString: @"formaction"])
|
||||
{
|
||||
value = [_attributes valueAtIndex: count];
|
||||
skipAttribute = ([value rangeOfString: @"://"].location
|
||||
== NSNotFound
|
||||
&& ![value hasPrefix: @"mailto:"]
|
||||
&& ![value hasPrefix: @"#"]);
|
||||
lowerValue = [[value lowercaseString] stringByReplacingString: @"\""
|
||||
withString: @""];
|
||||
skipAttribute =
|
||||
([lowerValue rangeOfString: @"://"].location == NSNotFound
|
||||
&& ![lowerValue hasPrefix: @"mailto:"]
|
||||
&& ![lowerValue hasPrefix: @"#"])
|
||||
|| [lowerValue rangeOfString: @"javascript:"].location != NSNotFound;
|
||||
if (!skipAttribute)
|
||||
[resultPart appendString: @" rel=\"noopener\""];
|
||||
}
|
||||
// Avoid: <div style="background:url('http://www.sogo.nu/fileadmin/sogo/logos/sogo.bts.png' ); width: 200px; height: 200px;" title="ssss">
|
||||
else if ([name isEqualToString: @"style"])
|
||||
@@ -385,39 +392,6 @@ _xmlCharsetForCharset (NSString *charset)
|
||||
if ([value rangeOfString: @"url" options: NSCaseInsensitiveSearch].location != NSNotFound)
|
||||
name = [NSString stringWithFormat: @"unsafe-%@", name];
|
||||
}
|
||||
else if (
|
||||
// Mouse Events
|
||||
[name isEqualToString: @"onclick"] ||
|
||||
[name isEqualToString: @"ondblclick"] ||
|
||||
[name isEqualToString: @"onmousedown"] ||
|
||||
[name isEqualToString: @"onmousemove"] ||
|
||||
[name isEqualToString: @"onmouseout"] ||
|
||||
[name isEqualToString: @"onmouseup"] ||
|
||||
[name isEqualToString: @"onmouseover"] ||
|
||||
|
||||
// Keyboard Events
|
||||
[name isEqualToString: @"onkeydown"] ||
|
||||
[name isEqualToString: @"onkeypress"] ||
|
||||
[name isEqualToString: @"onkeyup"] ||
|
||||
|
||||
// Frame/Object Events
|
||||
[name isEqualToString: @"onabort"] ||
|
||||
[name isEqualToString: @"onerror"] ||
|
||||
[name isEqualToString: @"onload"] ||
|
||||
[name isEqualToString: @"onresize"] ||
|
||||
[name isEqualToString: @"onscroll"] ||
|
||||
[name isEqualToString: @"onunload"] ||
|
||||
|
||||
// Form Events
|
||||
[name isEqualToString: @"onblur"] ||
|
||||
[name isEqualToString: @"onchange"] ||
|
||||
[name isEqualToString: @"onfocus"] ||
|
||||
[name isEqualToString: @"onreset"] ||
|
||||
[name isEqualToString: @"onselect"] ||
|
||||
[name isEqualToString: @"onsubmit"])
|
||||
{
|
||||
skipAttribute = YES;
|
||||
}
|
||||
else
|
||||
value = [_attributes valueAtIndex: count];
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
|
||||
s = [[info objectForKey:@"disposition"] objectForKey: @"type"];
|
||||
|
||||
shouldDisplay = (s && ([s caseInsensitiveCompare: @"ATTACHMENT"]
|
||||
== NSOrderedSame));
|
||||
shouldDisplay = (s && ([s caseInsensitiveCompare: @"ATTACHMENT"] == NSOrderedSame));
|
||||
|
||||
if (!shouldDisplay && !textPart)
|
||||
shouldDisplay = ([[info objectForKey: @"bodyId"] length] ? YES : NO);
|
||||
@@ -216,7 +215,7 @@ static BOOL showNamedTextAttachmentsInline = NO;
|
||||
// TIFF files aren't well-supported and Thunderbird sometimes send PDF
|
||||
// files over as image/pdf !
|
||||
if ([mt isEqualToString:@"image"] &&
|
||||
!([st isEqualToString: @"tiff"] || [st isEqualToString: @"pdf"]))
|
||||
!([st isEqualToString: @"tiff"] || [st isEqualToString: @"pdf"] || [st hasSuffix: @"xml"]))
|
||||
{
|
||||
if ([self _shouldDisplayAsAttachment: _info textPart: NO])
|
||||
return [self linkViewer];
|
||||
|
||||
@@ -65,43 +65,43 @@
|
||||
/* accessors */
|
||||
|
||||
- (void)setTooltip {
|
||||
self->formatAction = @selector(tooltipForApt::);
|
||||
self->formatAction = @selector(tooltipForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setSingleLineFullDetails {
|
||||
self->formatAction = @selector(singleLineFullDetailsForApt::);
|
||||
self->formatAction = @selector(singleLineFullDetailsForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setFullDetails {
|
||||
self->formatAction = @selector(fullDetailsForApt::);
|
||||
self->formatAction = @selector(fullDetailsForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setPrivateTooltip {
|
||||
self->formatAction = @selector(tooltipForPrivateApt::);
|
||||
self->formatAction = @selector(tooltipForPrivateApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setPrivateDetails {
|
||||
self->formatAction = @selector(detailsForPrivateApt::);
|
||||
self->formatAction = @selector(detailsForPrivateApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setTitleOnly {
|
||||
self->formatAction = @selector(titleForApt::);
|
||||
self->formatAction = @selector(titleForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setShortTitleOnly {
|
||||
self->formatAction = @selector(shortTitleForApt::);
|
||||
self->formatAction = @selector(shortTitleForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setShortMonthTitleOnly {
|
||||
self->formatAction = @selector(shortMonthTitleForApt::);
|
||||
self->formatAction = @selector(shortMonthTitleForApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setPrivateSuppressAll {
|
||||
self->formatAction = @selector(suppressApt::);
|
||||
self->formatAction = @selector(suppressApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setPrivateTitleOnly {
|
||||
self->formatAction = @selector(titleOnlyForPrivateApt::);
|
||||
self->formatAction = @selector(titleOnlyForPrivateApt:_refDate:);
|
||||
}
|
||||
|
||||
- (void)setPrivateTitle:(NSString *)_privateTitle {
|
||||
@@ -218,7 +218,7 @@
|
||||
|
||||
- (NSString *)titleForApt:(id)_apt :(NSCalendarDate *)_refDate {
|
||||
NSString *title;
|
||||
|
||||
|
||||
title = [_apt valueForKey:@"title"];
|
||||
if (![title isNotEmpty])
|
||||
title = [self titlePlaceholder];
|
||||
@@ -227,11 +227,11 @@
|
||||
|
||||
- (NSString *)shortTitleForApt:(id)_apt :(NSCalendarDate *)_refDate {
|
||||
NSString *title;
|
||||
|
||||
|
||||
title = [self titleForApt:_apt :_refDate];
|
||||
if ([title length] > 50)
|
||||
title = [[title substringToIndex: 49] stringByAppendingString:@"..."];
|
||||
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -248,14 +248,14 @@
|
||||
[self appendTimeInfoForDate: startDate usingReferenceDate: nil
|
||||
toBuffer: title];
|
||||
[title appendFormat: @" %@", [self titleForApt:_apt :_refDate]];
|
||||
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
- (NSString *)singleLineFullDetailsForApt:(id)_apt :(NSCalendarDate *)_refDate {
|
||||
NSMutableString *aptDescr;
|
||||
NSString *s;
|
||||
|
||||
|
||||
aptDescr = [NSMutableString stringWithCapacity:60];
|
||||
[self appendTimeInfoFromApt:_apt
|
||||
usingReferenceDate:_refDate
|
||||
@@ -288,7 +288,7 @@
|
||||
s = [self shortTitleForApt: _apt : _refDate];
|
||||
if ([s length] > 0)
|
||||
[aptDescr appendFormat:@"<br />%@", s];
|
||||
|
||||
|
||||
return aptDescr;
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
: (NSCalendarDate *) _refDate
|
||||
{
|
||||
NSString *s;
|
||||
|
||||
|
||||
s = [self privateTitle];
|
||||
if (!s)
|
||||
s = @"";
|
||||
@@ -348,11 +348,11 @@
|
||||
{
|
||||
NSMutableString *aptDescr;
|
||||
NSString *s;
|
||||
|
||||
|
||||
aptDescr = [NSMutableString stringWithCapacity: 25];
|
||||
[self appendTimeInfoFromApt: _apt
|
||||
usingReferenceDate: _refDate
|
||||
toBuffer: aptDescr];
|
||||
toBuffer: aptDescr];
|
||||
if ((s = [self privateTitle]) != nil)
|
||||
[aptDescr appendFormat:@"\n%@", s];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2005-2013 Inverse inc.
|
||||
Copyright (C) 2005-2022 Inverse inc.
|
||||
Copyright (C) 2005 SKYRIX Software AG
|
||||
|
||||
This file is part of SOGo.
|
||||
@@ -425,7 +425,8 @@ DIV.mailer_mailcontent
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
overflow: auto; }
|
||||
overflow: auto;
|
||||
transform: translateX(0); }
|
||||
|
||||
DIV.mailer_mailcontent TABLE
|
||||
{
|
||||
|
||||
@@ -7,11 +7,6 @@ function initLogin() {
|
||||
date.setTime(date.getTime() - 86400000);
|
||||
|
||||
var href = $("connectForm").action.split("/");
|
||||
var appName = href[href.length-2];
|
||||
|
||||
document.cookie = ("0xHIGHFLYxSOGo=discarded"
|
||||
+ "; expires=" + date.toGMTString()
|
||||
+ "; path=/" + appName + "/");
|
||||
|
||||
var about = $("about");
|
||||
if (about) {
|
||||
@@ -118,9 +113,7 @@ function onLoginCallback(http) {
|
||||
|
||||
if (http.status == 200) {
|
||||
// Make sure browser's cookies are enabled
|
||||
var loginCookie = readLoginCookie();
|
||||
|
||||
if (!loginCookie) {
|
||||
if (navigator && !navigator.cookieEnabled) {
|
||||
SetLogMessage("errorMessage", _("cookiesNotEnabled"));
|
||||
submitBtn.disabled = false;
|
||||
return;
|
||||
|
||||
@@ -2329,17 +2329,6 @@ function readCookie(name) {
|
||||
return foundCookie;
|
||||
}
|
||||
|
||||
function readLoginCookie() {
|
||||
var loginValues = null;
|
||||
var cookie = readCookie("0xHIGHFLYxSOGo");
|
||||
if (cookie && cookie.length > 8) {
|
||||
var value = decodeURIComponent(cookie.substr(8));
|
||||
loginValues = value.base64decode().split(":");
|
||||
}
|
||||
|
||||
return loginValues;
|
||||
}
|
||||
|
||||
/* logging widgets */
|
||||
function SetLogMessage(containerId, message, msgType) {
|
||||
var container = $(containerId);
|
||||
|
||||
2
Version
2
Version
@@ -1,3 +1,3 @@
|
||||
MAJOR_VERSION=2
|
||||
MINOR_VERSION=4
|
||||
SUBMINOR_VERSION=1
|
||||
SUBMINOR_VERSION=2
|
||||
|
||||
@@ -8,9 +8,8 @@ Standards-Version: 3.9.1
|
||||
Package: sogo
|
||||
Section: web
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, tmpreaper, sope4.9-libxmlsaxdriver, sope4.9-db-connector, gnustep-make, libcurl3 | libcurl4, zip, liblasso3 (>= 2.3.5)
|
||||
Recommends: memcached
|
||||
Suggests: nginx
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, sope4.9-libxmlsaxdriver, sope4.9-db-connector, gnustep-make, libcurl3 | libcurl4, zip, liblasso3 (>= 2.3.5)
|
||||
Recommends: memcached, apache2 | nginx | httpd
|
||||
Description: a modern and scalable groupware
|
||||
SOGo is a groupware server built around OpenGroupware.org (OGo) and
|
||||
the SOPE application server with focus on scalability.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# SOGOSPOOL must match the value of the configuration parameter SOGoMailSpoolPath
|
||||
SOGOSPOOL=/var/spool/sogo
|
||||
|
||||
/usr/sbin/tmpreaper 24 "$SOGOSPOOL"
|
||||
find "$SOGOSPOOL" -type f -user sogo -atime +23 -delete > /dev/null
|
||||
find "$SOGOSPOOL" -mindepth 1 -type d -user sogo -empty -delete > /dev/null
|
||||
|
||||
@@ -16,7 +16,7 @@ Group: Productivity/Groupware
|
||||
Source: SOGo-%{sogo_version}.tar.gz
|
||||
Prefix: /usr
|
||||
AutoReqProv: off
|
||||
Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, lasso, libmemcached, memcached, libcurl, tmpwatch, zip
|
||||
Requires: gnustep-base >= 1.23, sope%{sope_major_version}%{sope_minor_version}-core, httpd, sope%{sope_major_version}%{sope_minor_version}-core, sope%{sope_major_version}%{sope_minor_version}-appserver, sope%{sope_major_version}%{sope_minor_version}-ldap, sope%{sope_major_version}%{sope_minor_version}-cards >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-gdl1-contentstore >= %{sogo_version}, sope%{sope_major_version}%{sope_minor_version}-sbjson, lasso, libmemcached, memcached, libcurl, zip
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}
|
||||
BuildRequires: gcc-objc gnustep-base gnustep-make sope%{sope_major_version}%{sope_minor_version}-appserver-devel sope%{sope_major_version}%{sope_minor_version}-core-devel sope%{sope_major_version}%{sope_minor_version}-ldap-devel sope%{sope_major_version}%{sope_minor_version}-mime-devel sope%{sope_major_version}%{sope_minor_version}-xml-devel sope%{sope_major_version}%{sope_minor_version}-gdl1-devel sope%{sope_major_version}%{sope_minor_version}-sbjson-devel lasso-devel libmemcached-devel sed libcurl-devel
|
||||
|
||||
|
||||
Reference in New Issue
Block a user