Merge to 2.4.2

This commit is contained in:
Francis Lachapelle
2022-04-04 18:47:43 -04:00
69 changed files with 2861 additions and 6212 deletions

View File

@@ -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];

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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 */

View File

@@ -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

View File

@@ -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];

View File

@@ -48,7 +48,7 @@
@class NGImap4Envelope;
@class NGImap4EnvelopeAddress;
NSArray *SOGoMailCoreInfoKeys;
extern NSArray *SOGoMailCoreInfoKeys;
@interface SOGoMailObject : SOGoMailBaseObject
{

View File

@@ -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;

View File

@@ -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

View File

@@ -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
{

View File

@@ -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]];

View File

@@ -776,7 +776,7 @@
if (channel)
{
lowerFilter = [filter lowercaseString];
lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"];
lowerFilter = [lowerFilter asSafeSQLLikeString];
sql = [NSMutableString stringWithFormat: (@"SELECT *"
@" FROM %@"

View File

@@ -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."

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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&param2=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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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
View 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('<', '&lt;').replace('>', '&gt;') },
...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
View 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"
}
}

View 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
View 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
View 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
}
})
})

View 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 })
})
})

View 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({})
})
})

View 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)
})
})

View 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)
}
}
})
})

View 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)
})
})

View 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)
})
})

View 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')
})
})

View 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)
})
})

View 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
View 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)
})
})
})
})

View 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)
})
})

View File

@@ -0,0 +1,14 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"requires": [
"esm"
],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@@ -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];

View File

@@ -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];

View File

@@ -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];

View File

@@ -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
{

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,3 +1,3 @@
MAJOR_VERSION=2
MINOR_VERSION=4
SUBMINOR_VERSION=1
SUBMINOR_VERSION=2

View File

@@ -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.

View File

@@ -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

View File

@@ -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