mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-08-25 08:23:28 +00:00
merge of '0ce8c0fe1af6188f22754102040d63f124d3761b'
and '98ba3d5ee1d111284dfb68999ac43af19ef48122' Monotone-Parent: 0ce8c0fe1af6188f22754102040d63f124d3761b Monotone-Parent: 98ba3d5ee1d111284dfb68999ac43af19ef48122 Monotone-Revision: bdac9c079f5f6f8a742d138d4a5f9d41043ae013 Monotone-Author: flachapelle@inverse.ca Monotone-Date: 2010-07-26T13:20:23 Monotone-Branch: ca.inverse.sogo
This commit is contained in:
@@ -1,3 +1,66 @@
|
||||
2010-07-22 Ludovic Marcotte <lmarcotte@inverse.ca>
|
||||
|
||||
* Added Migration/Horde/* - scripts used to migrate
|
||||
the address books and email signatures from Horde to
|
||||
the SOGo database.
|
||||
|
||||
2010-07-21 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* UI/WebServerResources/ContactsUI.js: (dropSelectedContacts):
|
||||
reenabled copying contacts from system (LDAP) addressbooks.
|
||||
|
||||
2010-07-20 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* SoObjects/SOGo/SOGoUserFolder.m (_subFoldersFromFolder:): we
|
||||
need to validate the current user's access to the listed folders
|
||||
again because of the recent changes on SOGoParentFolder.
|
||||
|
||||
* UI/Contacts/UIxContactFoldersView.m (_subFoldersFromFolder:):
|
||||
removed unused method.
|
||||
|
||||
2010-07-16 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* UI/Contacts/UIxContactEditor.m (-photosURL): copy of the method
|
||||
below.
|
||||
|
||||
* UI/Contacts/UIxContactView.m (-photosURL): new access that
|
||||
returns the URL to the photos contained in the VCARD, whether
|
||||
inline or not.
|
||||
(-tabSelection): removed unused method.
|
||||
|
||||
* SoObjects/Contacts/SOGoContactGCSEntry.m
|
||||
(-lookupName:inContext:acquire:): new overriden method to handle
|
||||
"photoX" lookups and return the corrsponding SOGoContactEntryPhoto
|
||||
instance, if exists.
|
||||
|
||||
* SoObjects/Contacts/SOGoContactEntryPhoto.[hm]: new controller
|
||||
class for VCARD PHOTO objects.
|
||||
|
||||
* SoObjects/SOGo/SOGoContentObject.m (-davContentLength): fixed a
|
||||
crash occurring with certain versions of GNUstep by using
|
||||
NSISOLatin1StringEncoding instead of UTF8. This is actually the
|
||||
correct way of doing things anyway since we want to return the
|
||||
length in bytes and not in characters.
|
||||
|
||||
2010-07-16 Ludovic Marcotte <lmarcotte@inverse.ca>
|
||||
|
||||
* UI/Contacts/UIxContactView.m
|
||||
In secondaryEmail, we make sure that we loop
|
||||
in all available email address and we pick the
|
||||
first one that doesn't match the primaryEmail
|
||||
* UI/Contacts/UIxContactEditor.m
|
||||
UI/Contacts/UIxContactView.m - we make sure,
|
||||
when excluding a type, that we reset the
|
||||
local variable to nil since we might exclude
|
||||
all values in our array.
|
||||
|
||||
2010-07-16 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* SoObjects/Appointments/SOGoAppointmentObject.m
|
||||
(_handleUpdatedEvent:fromOldEvent:): added handling of delegation
|
||||
chains when resetting the participation status of the attendees.
|
||||
Fixes #688.
|
||||
|
||||
2010-07-15 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* SoObjects/SOGo/SOGoUser.m (-personalCalendarFolderInContext:):
|
||||
@@ -11,6 +74,7 @@
|
||||
|
||||
* UI/Scheduler/UIxCalListingActions.m
|
||||
(_aptFolder:withClientObject:): removed unused method.
|
||||
(_fixDates:): apply fix also for monthly views.
|
||||
|
||||
* UI/WebServerResources/UIxAppointmentEditor.js
|
||||
(onComposeToAllAttendees): take the status image DIV into account
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Version file
|
||||
|
||||
SUBMINOR_VERSION:=2
|
||||
SUBMINOR_VERSION:=0
|
||||
|
||||
# v0.9.32 requires libSOGo v0.9.57
|
||||
# v0.9.24 requires libWEExtensions v4.5.67
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import PHPDeserializer
|
||||
import sys
|
||||
|
||||
class HordeSignatureConverter:
|
||||
def __init__(self, user, domain):
|
||||
self.user = user
|
||||
self.domain = domain
|
||||
self.domainLen = len(domain)
|
||||
|
||||
def fetchSignatures(self, conn):
|
||||
self.signatures = None
|
||||
self.conn = conn
|
||||
self.fetchIdentities()
|
||||
|
||||
return self.signatures
|
||||
|
||||
def fetchIdentities(self):
|
||||
self.users = {}
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
if self.user == "ALL":
|
||||
userClause = ""
|
||||
else:
|
||||
userClause = "AND pref_uid = '%s'" % self.user
|
||||
query = "SELECT pref_uid, pref_value" \
|
||||
" FROM horde_prefs" \
|
||||
" WHERE pref_scope = 'horde'" \
|
||||
" AND pref_name = 'identities'" \
|
||||
" %s" % userClause
|
||||
cursor.execute(query)
|
||||
|
||||
self.signatures = {}
|
||||
records = cursor.fetchall()
|
||||
max = len(records)
|
||||
if max > 0:
|
||||
for record in records:
|
||||
user = record[0]
|
||||
signature = self.decodeSignature(record[1], user)
|
||||
if signature is None or len(signature.strip()) == 0:
|
||||
print "No useful signature found for %s" % user
|
||||
else:
|
||||
self.signatures[user] = signature
|
||||
|
||||
print "%d useful signature(s) found in %d record(s)" % (len(self.signatures), max)
|
||||
else:
|
||||
print "No record found"
|
||||
|
||||
cursor.close()
|
||||
|
||||
def decodeSignature(self, prefs, user):
|
||||
des = PHPDeserializer.PHPDeserializer(prefs)
|
||||
identities = des.deserialize()
|
||||
nbrEntries = len(identities)
|
||||
signatures = []
|
||||
for identity in identities:
|
||||
fromAddr = identity["from_addr"]
|
||||
if (len(fromAddr) > self.domainLen
|
||||
and fromAddr[-self.domainLen:] == self.domain):
|
||||
if identity.has_key("signature"):
|
||||
signatures.append(identity["signature"])
|
||||
|
||||
if len(signatures) > 0:
|
||||
signature = self.chooseSignature(signatures)
|
||||
else:
|
||||
signature = None
|
||||
|
||||
return signature
|
||||
|
||||
def chooseSignature(self, signatures):
|
||||
biggest = -1
|
||||
length = -1
|
||||
count = 0
|
||||
for signature in signatures:
|
||||
thisLength = len(signature)
|
||||
if thisLength > 0 and thisLength > length:
|
||||
biggest = count
|
||||
count = count + 1
|
||||
|
||||
if biggest == -1:
|
||||
signature = None
|
||||
else:
|
||||
signature = signatures[biggest]
|
||||
|
||||
return signature
|
||||
@@ -0,0 +1,135 @@
|
||||
# we perform no validation
|
||||
class PHPDeserializer:
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
if string is None:
|
||||
self.length = 0
|
||||
else:
|
||||
self.length = len(string)
|
||||
self.cursor = 0
|
||||
|
||||
def deserializeInteger(self):
|
||||
start = self.cursor
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
if self.cursor < self.length:
|
||||
currentChar = self.string[self.cursor]
|
||||
if currentChar.isdigit():
|
||||
self.cursor = self.cursor + 1
|
||||
else:
|
||||
done = True
|
||||
else:
|
||||
done = True
|
||||
|
||||
length = self.cursor - start
|
||||
if length > 0:
|
||||
dInteger = int(self.string[start:self.cursor])
|
||||
else:
|
||||
dInteger = 0
|
||||
|
||||
return dInteger
|
||||
|
||||
def deserializeBoolean(self):
|
||||
start = self.cursor
|
||||
|
||||
if self.cursor < self.length:
|
||||
currentChar = self.string[self.cursor]
|
||||
if currentChar == "0":
|
||||
dBoolean = False
|
||||
else:
|
||||
dBoolean = True
|
||||
self.cursor = self.cursor + 1
|
||||
else:
|
||||
dBoolean = None
|
||||
|
||||
return dBoolean
|
||||
|
||||
def deserializeString(self):
|
||||
length = self.deserializeInteger()
|
||||
start = self.cursor + 2
|
||||
end = start+length
|
||||
value = self.string[start:end]
|
||||
self.cursor = end + 1
|
||||
|
||||
return value
|
||||
|
||||
def deserializeArray(self):
|
||||
isHash = False
|
||||
max = self.deserializeInteger()
|
||||
dArray = [ None ] * max
|
||||
|
||||
self.cursor = self.cursor + 2
|
||||
count = 0
|
||||
while count < max:
|
||||
elementIndex = self.deserialize()
|
||||
self.cursor = self.cursor + 1
|
||||
element = self.deserialize()
|
||||
if isHash:
|
||||
if type(elementIndex) == int:
|
||||
elementIndex = "%d" % elementIndex
|
||||
else:
|
||||
if type(elementIndex) != int:
|
||||
dArray = self._arrayToHash(dArray)
|
||||
isHash = True
|
||||
dArray[elementIndex] = element
|
||||
self.cursor = self.cursor + 1
|
||||
count = count + 1
|
||||
|
||||
if self.string[self.cursor] != "}":
|
||||
raise Exception, \
|
||||
("inconsistency detected in serialized string at character %d:\n%s"
|
||||
% (self.cursor, self._exceptionSample()))
|
||||
|
||||
return dArray
|
||||
|
||||
def _exceptionSample(self):
|
||||
if self.cursor > 30:
|
||||
start = self.cursor - 30
|
||||
prefix = "..."
|
||||
else:
|
||||
start = 0
|
||||
prefix = ""
|
||||
carret = self.cursor + len(prefix) - start
|
||||
length = len(self.string)
|
||||
if self.cursor + 30 < length:
|
||||
end = self.cursor + 30
|
||||
suffix = "..."
|
||||
else:
|
||||
end = length
|
||||
suffix = ""
|
||||
|
||||
sample = self.string[start:end]
|
||||
while sample[0] == " ":
|
||||
carret = carret - 1
|
||||
sample = sample[1:]
|
||||
|
||||
return "%s%s%s\n%s^" % (prefix, sample, suffix, carret * " ")
|
||||
|
||||
def _arrayToHash(self, array):
|
||||
dHash = {}
|
||||
count = 0
|
||||
for element in array:
|
||||
dHash["%d" % count] = element
|
||||
count = count + 1
|
||||
|
||||
return dHash
|
||||
|
||||
def deserialize(self):
|
||||
dObject = None
|
||||
|
||||
if self.string is not None and self.length > 0:
|
||||
currentChar = self.string[self.cursor]
|
||||
self.cursor = self.cursor + 2
|
||||
if currentChar == 'a':
|
||||
dObject = self.deserializeArray()
|
||||
elif currentChar == 's':
|
||||
dObject = self.deserializeString()
|
||||
elif currentChar == 'i':
|
||||
dObject = self.deserializeInteger()
|
||||
elif currentChar == 'b':
|
||||
dObject = self.deserializeBoolean()
|
||||
elif currentChar == 'N':
|
||||
dObject = None
|
||||
|
||||
return dObject
|
||||
@@ -0,0 +1,7 @@
|
||||
1) copy config.py.in to config.py and personalize the settings
|
||||
2) personalize the domain setting in signature.py (look for DOMAIN.COM)
|
||||
3) the scripts require "webdavlib.py", which is located in <SOGoDIR>/Tests/Integration/. Therefore, in order to use the scripts, we must set the environment variable "PYTHONPATH" to that directory on our system.
|
||||
4) invoke "signature <USER>" for importing signatures
|
||||
5) invoke "turba <USER>" for importing signatures
|
||||
6) "<USER>" can have the special "ALL" value
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
import PHPDeserializer
|
||||
import webdavlib
|
||||
import sys
|
||||
|
||||
commonMappings = { "owner_id": "owner",
|
||||
"object_id": "filename",
|
||||
"object_uid": "uid",
|
||||
"object_name": "fn" }
|
||||
cardMappings = { "object_alias": "nickname",
|
||||
"object_email": "email",
|
||||
"object_homeaddress": "homeaddress",
|
||||
"object_homephone": "homephone",
|
||||
"object_workaddress": "workaddress",
|
||||
"object_workphone": "workphone",
|
||||
"object_cellphone": "cellphone",
|
||||
"object_fax": "fax",
|
||||
"object_title": "title",
|
||||
"object_company": "org",
|
||||
"object_notes": "notes",
|
||||
"object_freebusyurl": "fburl" }
|
||||
|
||||
prodid = "-//Inverse inc.//SOGo Turba Importer 1.0//EN"
|
||||
|
||||
# a managed type of template where each line is put only if at least one field
|
||||
# has been filled
|
||||
cardTemplate = u"""BEGIN:VCARD\r
|
||||
VERSION:3.0\r
|
||||
PRODID:%s\r
|
||||
UID:${uid}\r
|
||||
FN:${fn}\r
|
||||
TITLE:${title}\r
|
||||
ORG:${org};\r
|
||||
NICKNAME:${nickname}\r
|
||||
EMAIL:${email}\r
|
||||
ADR;TYPE=work:;;${workaddress};;;;\r
|
||||
ADR;TYPE=home:;;${homeaddress};;;;\r
|
||||
TEL;TYPE=work:${workphone}\r
|
||||
TEL;TYPE=home:${homephone}\r
|
||||
TEL;TYPE=fax:${fax}\r
|
||||
NOTE:${notes}\r
|
||||
FBURL:${fburl}\r
|
||||
END:VCARD""" % prodid
|
||||
|
||||
class TurbaConverter:
|
||||
def __init__(self, user, webdavConfig):
|
||||
self.user = user
|
||||
self.webdavConfig = webdavConfig
|
||||
|
||||
def start(self, conn):
|
||||
self.conn = conn
|
||||
self.readUsers()
|
||||
self.missing = []
|
||||
for user in self.users.keys():
|
||||
self.hasCards = False
|
||||
self.hasLists = False
|
||||
self.currentUser = user
|
||||
self.readUserRecords()
|
||||
if self.hasCards or self.hasLists:
|
||||
print "Converting addressbook of '%s'" % user
|
||||
self.prepareCards()
|
||||
self.uploadCards()
|
||||
self.prepareLists()
|
||||
self.uploadLists()
|
||||
else:
|
||||
self.missing.append(user)
|
||||
|
||||
if len(self.missing) > 0:
|
||||
print "No information extracted for: %s" % ", ".join(self.missing)
|
||||
|
||||
print "Done"
|
||||
|
||||
def readUsers(self):
|
||||
self.users = {}
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
query = "SELECT user_uid, datatree_name FROM horde_datatree"
|
||||
if self.user != "ALL":
|
||||
query = query + " WHERE user_uid = '%s'" % self.user
|
||||
cursor.execute(query)
|
||||
|
||||
records = cursor.fetchall()
|
||||
count = 0
|
||||
max = len(records)
|
||||
for record in records:
|
||||
record_user = record[0].lower()
|
||||
if not self.users.has_key(record_user):
|
||||
self.users[record_user] = []
|
||||
self.users[record_user].append(record[1])
|
||||
count = count + 1
|
||||
cursor.close()
|
||||
|
||||
def readUserRecords(self):
|
||||
self.cards = {}
|
||||
self.lists = {}
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
owner_ids = self.users[self.currentUser]
|
||||
whereClause = "owner_id = '%s'" % "' or owner_id = '".join(owner_ids)
|
||||
query = "SELECT * FROM turba_objects WHERE %s" % whereClause
|
||||
cursor.execute(query)
|
||||
self.prepareColumns(cursor)
|
||||
|
||||
records = cursor.fetchall()
|
||||
count = 0
|
||||
max = len(records)
|
||||
while count < max:
|
||||
self.parseRecord(records[count])
|
||||
count = count + 1
|
||||
|
||||
cursor.close()
|
||||
|
||||
def prepareColumns(self, cursor):
|
||||
self.columns = {}
|
||||
count = 0
|
||||
for dbColumn in cursor.description:
|
||||
columnId = dbColumn[0]
|
||||
self.columns[columnId] = count
|
||||
count = count + 1
|
||||
|
||||
def parseRecord(self, record):
|
||||
typeCol = self.columns["object_type"]
|
||||
meta = {}
|
||||
self.applyRecordMappings(meta, record, commonMappings)
|
||||
|
||||
if record[typeCol] == "Object":
|
||||
meta["type"] = "card"
|
||||
self.hasCards = True
|
||||
self.applyRecordMappings(meta, record, cardMappings)
|
||||
elif record[typeCol] == "Group":
|
||||
meta["type"] = "list"
|
||||
self.hasLists = True
|
||||
self.fillListMembers(meta, record)
|
||||
else:
|
||||
raise Exception, "UNKNOWN TYPE: %s" % record[type]
|
||||
|
||||
self.dispatchMeta(meta)
|
||||
|
||||
def applyRecordMappings(self, meta, record, mappings):
|
||||
for k in mappings.keys():
|
||||
metaKey = mappings[k]
|
||||
meta[metaKey] = self.recordColumn(record, k)
|
||||
|
||||
def recordColumn(self, record, columnName):
|
||||
columnIndex = self.columns[columnName]
|
||||
value = record[columnIndex]
|
||||
if value is None:
|
||||
value = u""
|
||||
else:
|
||||
value = self.deUTF8Ize(value)
|
||||
|
||||
return value
|
||||
|
||||
def deUTF8Ize(self, value):
|
||||
# unicode -> repeat(utf-8 str -> iso-8859-1 str) -> unicode
|
||||
oldValue = value
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
try:
|
||||
utf8Value = value.encode("iso-8859-1")
|
||||
value = utf8Value.decode("utf-8")
|
||||
except:
|
||||
done = True
|
||||
if value == oldValue:
|
||||
done = True
|
||||
|
||||
return value
|
||||
|
||||
def fillListMembers(self, meta, record):
|
||||
members = self.recordColumn(record, "object_members")
|
||||
if members is not None and len(members) > 0:
|
||||
deserializer = PHPDeserializer.PHPDeserializer(members)
|
||||
dMembers = deserializer.deserialize()
|
||||
else:
|
||||
dMembers = []
|
||||
meta["members"] = dMembers
|
||||
|
||||
def dispatchMeta(self, meta):
|
||||
owner = meta["owner"]
|
||||
if meta["type"] == "card":
|
||||
repository = self.cards
|
||||
else:
|
||||
repository = self.lists
|
||||
filename = meta["filename"]
|
||||
repository[filename] = meta
|
||||
|
||||
def prepareCards(self):
|
||||
count = 0
|
||||
for filename in self.cards.keys():
|
||||
card = self.cards[filename]
|
||||
card["data"] = self.buildVCard(card).encode("utf-8")
|
||||
count = count + 1
|
||||
if count > 0:
|
||||
print " prepared %d cards" % count
|
||||
|
||||
def buildVCard(self, card):
|
||||
vcardArray = []
|
||||
tmplArray = cardTemplate.split("\r\n")
|
||||
for line in tmplArray:
|
||||
keyPos = line.find("${")
|
||||
if keyPos > -1:
|
||||
keyEndPos = line.find("}")
|
||||
key = line[keyPos+2:keyEndPos]
|
||||
if card.has_key(key):
|
||||
value = card[key]
|
||||
if len(value) > 0:
|
||||
newLine = "%s%s%s" % (line[0:keyPos],
|
||||
value.replace(";", "\;"),
|
||||
line[keyEndPos + 1:])
|
||||
vcardArray.append(self.foldLineIfNeeded(newLine))
|
||||
else:
|
||||
vcardArray.append(self.foldLineIfNeeded(line))
|
||||
|
||||
return "\r\n".join(vcardArray)
|
||||
|
||||
def foldLineIfNeeded(self, line):
|
||||
wasFolded = False
|
||||
newLine = line\
|
||||
.replace("\\", "\\\\") \
|
||||
.replace("\r", "\\r") \
|
||||
.replace("\n", "\\n")
|
||||
lines = []
|
||||
while len(newLine) > 73:
|
||||
wasFolded = True
|
||||
lines.append(newLine[0:73])
|
||||
newLine = newLine[73:]
|
||||
lines.append(newLine)
|
||||
|
||||
newLine = "\r\n ".join(lines)
|
||||
if wasFolded:
|
||||
print "line was folded: '%s' ->\n\n%s\n\n" % (line, newLine)
|
||||
|
||||
return newLine
|
||||
|
||||
def uploadCards(self):
|
||||
self.uploadEntries(self.cards,
|
||||
"vcf", "text/x-vcard; charset=utf-8");
|
||||
|
||||
def prepareLists(self):
|
||||
count = 0
|
||||
skipped = 0
|
||||
for filename in self.lists.keys():
|
||||
list = self.lists[filename]
|
||||
vlist = self.buildVList(list)
|
||||
if vlist is None:
|
||||
skipped = skipped + 1
|
||||
else:
|
||||
list["data"] = vlist.encode("utf-8")
|
||||
count = count + 1
|
||||
|
||||
if (count + skipped) > 0:
|
||||
print " prepared %d lists. %d were skipped." % (count, skipped)
|
||||
|
||||
def buildVList(self, list):
|
||||
vlist = None
|
||||
|
||||
members = list["members"]
|
||||
if len(members) > 0:
|
||||
cardMembers = []
|
||||
for member in members:
|
||||
card = self.getListCard(member)
|
||||
if card is not None:
|
||||
cardMembers.append(card)
|
||||
if len(cardMembers) > 0:
|
||||
vlist = self.assembleVList(list, cardMembers)
|
||||
else:
|
||||
print " list '%s' skipped because of lack of usable" \
|
||||
" members" % list["filename"]
|
||||
|
||||
return vlist
|
||||
|
||||
def getListCard(self, cardRef):
|
||||
card = None
|
||||
if len(cardRef) != 0 and not cardRef.startswith("localldap:"):
|
||||
if cardRef.startswith("localsql:"):
|
||||
cardRef = cardRef[9:]
|
||||
if self.cards.has_key(cardRef):
|
||||
card = self.cards[cardRef]
|
||||
else:
|
||||
print "card reference does not exist: '%s'" % cardRef
|
||||
|
||||
return card
|
||||
|
||||
def assembleVList(self, list, cardMembers):
|
||||
entries = []
|
||||
for cardMember in cardMembers:
|
||||
if cardMember.has_key("fn") and len(cardMember["fn"]) > 0:
|
||||
fn = ";FN=%s" % cardMember["fn"]
|
||||
else:
|
||||
fn = ""
|
||||
if cardMember.has_key("email") and len(cardMember["email"]) > 0:
|
||||
email = ";EMAIL=%s" % cardMember["email"]
|
||||
else:
|
||||
email = ""
|
||||
entries.append("CARD%s%s:%s.vcf"
|
||||
% (fn, email, cardMember["filename"]))
|
||||
if list.has_key("fn") and len(list["fn"]) > 0:
|
||||
listfn = "FN:%s\r\n" % list["fn"]
|
||||
else:
|
||||
listfn = ""
|
||||
vlist = """BEGIN:VLIST\r
|
||||
PRODID:%s\r
|
||||
VERSION:1.0\r
|
||||
UID:%s\r
|
||||
%s%s\r
|
||||
END:VLIST""" % (prodid, list["uid"], listfn, "\r\n".join(entries))
|
||||
|
||||
return vlist
|
||||
|
||||
def uploadLists(self):
|
||||
self.uploadEntries(self.lists,
|
||||
"vlf", "text/x-vcard; charset=utf-8");
|
||||
|
||||
def uploadEntries(self, entries, extension, mimeType):
|
||||
isatty = sys.stdout.isatty() # enable progressive display of summary
|
||||
success = 0
|
||||
failure = 0
|
||||
client = webdavlib.WebDAVClient(self.webdavConfig["hostname"],
|
||||
self.webdavConfig["port"],
|
||||
self.webdavConfig["username"],
|
||||
self.webdavConfig["password"])
|
||||
collection = '/SOGo/dav/%s/Contacts/personal' % self.currentUser
|
||||
|
||||
mkcol = webdavlib.WebDAVMKCOL(collection)
|
||||
client.execute(mkcol)
|
||||
|
||||
for entryName in entries.keys():
|
||||
entry = entries[entryName]
|
||||
if entry.has_key("data"):
|
||||
fullFilename = "%s.%s" % (entry["filename"], extension)
|
||||
url = "%s/%s" % (collection, fullFilename)
|
||||
put = webdavlib.HTTPPUT(url, entry["data"])
|
||||
put.content_type = mimeType
|
||||
client.execute(put)
|
||||
if (put.response["status"] < 200
|
||||
or put.response["status"] > 399):
|
||||
failure = failure + 1
|
||||
print " error uploading '%s': %d" \
|
||||
% (fullFilename, put.response["status"])
|
||||
else:
|
||||
success = success + 1
|
||||
if isatty:
|
||||
print "\r successes: %d; failures: %d" % (success, failure),
|
||||
if (success + failure) % 5 == 0:
|
||||
sys.stdout.flush()
|
||||
if isatty:
|
||||
print ""
|
||||
else:
|
||||
if (success + failure) > 0:
|
||||
print " successes: %d; failures: %d\n" % (success, failure)
|
||||
sys.stdout.flush()
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
webdavConfig = { "hostname": "sogo.hostname.com",
|
||||
"port": 80,
|
||||
"username": "username",
|
||||
"password": "password" }
|
||||
|
||||
dbConfig = { "host": "localhost",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"db": "database" }
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sys
|
||||
import MySQLdb
|
||||
import webdavlib
|
||||
|
||||
import HordeSignatureConverter
|
||||
|
||||
from config import webdavConfig, dbConfig
|
||||
|
||||
xmlns_inversedav = "urn:inverse:params:xml:ns:inverse-dav"
|
||||
|
||||
def UploadSignature(client, user, signature):
|
||||
collection = '/SOGo/dav/%s/' % user
|
||||
proppatch \
|
||||
= webdavlib.WebDAVPROPPATCH(collection,
|
||||
{ "{%s}signature" % xmlns_inversedav: \
|
||||
signature.encode("utf-8") })
|
||||
client.execute(proppatch)
|
||||
if (proppatch.response["status"] < 200
|
||||
or proppatch.response["status"] > 399):
|
||||
print "Failure uploading signature for user '%s': %d" \
|
||||
% (user, proppatch.response["status"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
user = sys.argv[1]
|
||||
else:
|
||||
raise Exception, "<user> argument must be specified" \
|
||||
" (use 'ALL' for everyone)"
|
||||
|
||||
conn = MySQLdb.connect(host = dbConfig["hostname"],
|
||||
user = dbConfig["username"],
|
||||
passwd = dbConfig["password"],
|
||||
db = dbConfig["database"],
|
||||
use_unicode = True)
|
||||
|
||||
cnv = HordeSignatureConverter.HordeSignatureConverter(user, "DOMAIN.COM")
|
||||
signatures = cnv.fetchSignatures(conn)
|
||||
conn.close()
|
||||
|
||||
client = webdavlib.WebDAVClient(webdavConfig["hostname"],
|
||||
webdavConfig["port"],
|
||||
webdavConfig["username"],
|
||||
webdavConfig["password"])
|
||||
for user in signatures:
|
||||
signature = signatures[user]
|
||||
UploadSignature(client, user, signature)
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import sys
|
||||
import MySQLdb
|
||||
|
||||
import TurbaConverter
|
||||
|
||||
from config import webdavConfig, dbConfig
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
user = sys.argv[1]
|
||||
else:
|
||||
raise Exception, "<user> argument must be specified" \
|
||||
" (use 'ALL' for everyone)"
|
||||
|
||||
conn = MySQLdb.connect(host = dbConfig["hostname"],
|
||||
user = dbConfig["username"],
|
||||
passwd = dbConfig["password"],
|
||||
db = dbConfig["database"],
|
||||
use_unicode = True)
|
||||
cnv = TurbaConverter.TurbaConverter(user, webdavConfig)
|
||||
cnv.start(conn)
|
||||
conn.close()
|
||||
@@ -1,4 +1,4 @@
|
||||
1.2-2010XXXX (1.2.3)
|
||||
1.3-20100721 (1.3.0)
|
||||
--------------------
|
||||
- added support for the "tentative" status in the invitation responses
|
||||
- inviting a group of contacts is now possible, where each contact will be
|
||||
@@ -12,16 +12,22 @@
|
||||
via the "/SOGo/dav/public" url
|
||||
- we now provide ICS and XML version of a user's personal calendars when
|
||||
accessed from his own "Calendar" base collection
|
||||
- events are now displayed with the coloured stripe representing their
|
||||
- events are now displayed with the colored stripe representing their
|
||||
category, if one is defined in the preferences
|
||||
- fixed display of all-day events in a monthly view where the timezone differs
|
||||
from the current one
|
||||
- the event location is now displayed in the calendar view when defined
|
||||
properly
|
||||
- added a caching mechanism for freebusy requests, in order to accelerate the
|
||||
display
|
||||
- added the ability to specify a time range when requesting a time slot
|
||||
suggestion
|
||||
- added live-loading support in the webmail interface with caching support
|
||||
- updated CKEditor and improved its integration with the current user
|
||||
language for automatic spell checking support
|
||||
- added support for displaying photos from contacts
|
||||
- added a Ukrainian translation
|
||||
- updated the Czeck translation
|
||||
- updated the Czech translation
|
||||
|
||||
1.2-20100504 (1.2.2)
|
||||
--------------------
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
2010-07-21 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* iCalXMLRenderer.m (_appendPaddingValues:withTag:intoString:):
|
||||
fixed a typo causing a crash.
|
||||
|
||||
2010-07-16 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* NGVCardPhoto.[hm]: new class module that implement facilities
|
||||
for handling "PHOTO" tags in vcards.
|
||||
|
||||
2010-06-08 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* iCalXMLRenderer.m (-[CardGroup xmlRender]): don't append empty
|
||||
|
||||
@@ -57,6 +57,7 @@ libNGCards_HEADER_FILES = \
|
||||
\
|
||||
NGVCard.h \
|
||||
NGVList.h \
|
||||
NGVCardPhoto.h \
|
||||
NGVCardReference.h \
|
||||
# NGVCardAddress.h \
|
||||
# NGVCardStrArrayValue.h \
|
||||
@@ -110,6 +111,7 @@ libNGCards_OBJC_FILES = \
|
||||
\
|
||||
NGVCard.m \
|
||||
NGVList.m \
|
||||
NGVCardPhoto.m \
|
||||
NGVCardReference.m \
|
||||
NGCardsSaxHandler.m \
|
||||
# IcalElements.m
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#import "NSArray+NGCards.h"
|
||||
|
||||
#import "NGVCardPhoto.h"
|
||||
|
||||
#import "NGVCard.h"
|
||||
|
||||
@implementation NGVCard
|
||||
@@ -79,6 +81,8 @@
|
||||
|| [classTag isEqualToString: @"TITLE"]
|
||||
|| [classTag isEqualToString: @"VERSION"])
|
||||
tagClass = [CardElement class];
|
||||
else if ([classTag isEqualToString: @"PHOTO"])
|
||||
tagClass = [NGVCardPhoto class];
|
||||
else
|
||||
tagClass = [super classForTag: classTag];
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/* NGVCardPhoto.h - this file is part of NGCards
|
||||
*
|
||||
* Copyright (C) 2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* NGCards 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.
|
||||
*
|
||||
* NGCards 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 NGCards; see the file COPYING. If not, write to the
|
||||
* Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
||||
* 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifndef NGVCARDPHOTO_H
|
||||
#define NGVCARDPHOTO_H
|
||||
|
||||
#import "CardElement.h"
|
||||
|
||||
@class NSData;
|
||||
@class NSString;
|
||||
|
||||
@interface NGVCardPhoto : CardElement
|
||||
|
||||
- (BOOL) isInline;
|
||||
|
||||
- (NSString *) type;
|
||||
|
||||
- (NSData *) decodedContent;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* NGVCARDPHOTO_H */
|
||||
@@ -0,0 +1,78 @@
|
||||
/* NGVCardPhoto.m - this file is part of NGCards
|
||||
*
|
||||
* Copyright (C) 2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* NGCards 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.
|
||||
*
|
||||
* NGCards 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 NGCards; see the file COPYING. If not, write to the
|
||||
* Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
||||
* 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import <Foundation/NSArray.h>
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
#import <NGExtensions/NGBase64Coding.h>
|
||||
#import <NGExtensions/NSObject+Logs.h>
|
||||
|
||||
#import "NGVCardPhoto.h"
|
||||
|
||||
@implementation NGVCardPhoto
|
||||
|
||||
- (BOOL) isInline
|
||||
{
|
||||
return ![[self value: 0 ofAttribute: @"value"] isEqualToString: @"uri"];
|
||||
}
|
||||
|
||||
- (NSString *) type
|
||||
{
|
||||
return [[self value: 0 ofAttribute: @"type"] uppercaseString];
|
||||
}
|
||||
|
||||
- (NSData *) decodedContent
|
||||
{
|
||||
NSString *encoding, *value;
|
||||
NSData *decodedContent;
|
||||
|
||||
decodedContent = nil;
|
||||
|
||||
if ([self isInline])
|
||||
{
|
||||
encoding = [[self value: 0 ofAttribute: @"encoding"] uppercaseString];
|
||||
if ([encoding isEqualToString: @"B"]
|
||||
|| [encoding isEqualToString: @"BASE64"])
|
||||
{
|
||||
/* We bypass -[values:] because we want to obtain the undecoded
|
||||
value first. */
|
||||
if ([values count] > 0)
|
||||
{
|
||||
value = [values objectAtIndex: 0];
|
||||
decodedContent = [value dataByDecodingBase64];
|
||||
}
|
||||
else
|
||||
[self errorWithFormat: @"attempt to decode empty value"];
|
||||
}
|
||||
else
|
||||
[self errorWithFormat:
|
||||
@"decoded content requested with an unknown encoding: '%@'",
|
||||
encoding];
|
||||
}
|
||||
else
|
||||
[self errorWithFormat:
|
||||
@"decoded content requested on a PHOTO of type 'uri'"];
|
||||
|
||||
return decodedContent;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -67,17 +67,14 @@
|
||||
|
||||
cmpTag = [aTag uppercaseString];
|
||||
|
||||
matchingElements = [NSMutableArray new];
|
||||
[matchingElements autorelease];
|
||||
matchingElements = [NSMutableArray arrayWithCapacity: 16];
|
||||
|
||||
allElements = [self objectEnumerator];
|
||||
currentElement = [allElements nextObject];
|
||||
while (currentElement)
|
||||
while ((currentElement = [allElements nextObject]))
|
||||
{
|
||||
currentTag = [[currentElement tag] uppercaseString];
|
||||
if ([currentTag isEqualToString: cmpTag])
|
||||
[matchingElements addObject: currentElement];
|
||||
currentElement = [allElements nextObject];
|
||||
}
|
||||
|
||||
return matchingElements;
|
||||
@@ -92,16 +89,12 @@
|
||||
|
||||
allElements = [self objectEnumerator];
|
||||
|
||||
matchingElements = [NSMutableArray new];
|
||||
[matchingElements autorelease];
|
||||
currentElement = [allElements nextObject];
|
||||
while (currentElement)
|
||||
{
|
||||
if ([currentElement hasAttribute: anAttribute
|
||||
havingValue: aValue])
|
||||
[matchingElements addObject: currentElement];
|
||||
currentElement = [allElements nextObject];
|
||||
}
|
||||
matchingElements = [NSMutableArray arrayWithCapacity: 16];
|
||||
|
||||
while ((currentElement = [allElements nextObject]))
|
||||
if ([currentElement hasAttribute: anAttribute
|
||||
havingValue: aValue])
|
||||
[matchingElements addObject: currentElement];
|
||||
|
||||
return matchingElements;
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
int count;
|
||||
|
||||
for (count = 0; count < max; count++)
|
||||
[rendering appendFormat: @"<%@/>"];
|
||||
[rendering appendFormat: @"<%@/>", valueTag];
|
||||
}
|
||||
|
||||
- (NSString *) _xmlRenderParameter: (NSString *) paramName
|
||||
|
||||
@@ -5638,7 +5638,15 @@ Index: sope-core/NGExtensions/ChangeLog
|
||||
===================================================================
|
||||
--- sope-core/NGExtensions/ChangeLog (revision 1664)
|
||||
+++ sope-core/NGExtensions/ChangeLog (working copy)
|
||||
@@ -1,3 +1,8 @@
|
||||
@@ -1,3 +1,16 @@
|
||||
+2010-07-16 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
+
|
||||
+ * NGBase64Coding.m (-dataByDecodingBase64)
|
||||
+ (-stringByDecodingBase64, -stringByEncodingBase64): make use of
|
||||
+ -[NSString lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]
|
||||
+ rather than -[... cStringLength] to avoid a crash within GNUstep.
|
||||
+ The latter is deprecated anyway...
|
||||
+
|
||||
+2010-01-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
+
|
||||
+ * NGRuleEngine.subproj/NGRuleModel.m (-candidateRulesForKey:):
|
||||
@@ -5647,6 +5655,40 @@ Index: sope-core/NGExtensions/ChangeLog
|
||||
2009-03-24 Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
|
||||
* NGQuotedPrintableCoding.m: encode '_' as '=5F', so that it is not
|
||||
Index: sope-core/NGExtensions/NGBase64Coding.m
|
||||
===================================================================
|
||||
--- sope-core/NGExtensions/NGBase64Coding.m (revision 1664)
|
||||
+++ sope-core/NGExtensions/NGBase64Coding.m (working copy)
|
||||
@@ -53,7 +53,7 @@
|
||||
size_t destLength = -1;
|
||||
char *dest, *src;
|
||||
|
||||
- if ((len = [self cStringLength]) == 0)
|
||||
+ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0)
|
||||
return @"";
|
||||
|
||||
destSize = (len + 2) / 3 * 4; // 3:4 conversion ratio
|
||||
@@ -91,7 +91,7 @@
|
||||
|
||||
if (StringClass == Nil) StringClass = [NSString class];
|
||||
|
||||
- if ((len = [self cStringLength]) == 0)
|
||||
+ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0)
|
||||
return @"";
|
||||
|
||||
destSize = (len / 4 + 1) * 3 + 1;
|
||||
@@ -135,9 +135,9 @@
|
||||
|
||||
if (StringClass == Nil) StringClass = [NSString class];
|
||||
|
||||
- if ((len = [self cStringLength]) == 0)
|
||||
+ if ((len = [self lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]) == 0)
|
||||
return [NSData data];
|
||||
-
|
||||
+
|
||||
destSize = (len / 4 + 1) * 3 + 1;
|
||||
dest = malloc(destSize + 1);
|
||||
|
||||
Index: sope-core/NGExtensions/NGRuleEngine.subproj/NGRuleModel.m
|
||||
===================================================================
|
||||
--- sope-core/NGExtensions/NGRuleEngine.subproj/NGRuleModel.m (revision 1664)
|
||||
|
||||
@@ -412,32 +412,6 @@ static SoSecurityManager *sm = nil;
|
||||
return componentSet;
|
||||
}
|
||||
|
||||
- (void) reloadWebCalendars: (BOOL) forceReload
|
||||
{
|
||||
NSArray *refs;
|
||||
SOGoWebAppointmentFolder *folder;
|
||||
SOGoUserSettings *us;
|
||||
NSDictionary *calSettings;
|
||||
NSString *ref;
|
||||
int count, max;
|
||||
|
||||
[self _migrateWebCalendarsSettings];
|
||||
us = [[SOGoUser userWithLogin: owner] userSettings];
|
||||
calSettings = [us objectForKey: @"Calendar"];
|
||||
refs = [[calSettings objectForKey: @"WebCalendars"] allKeys];
|
||||
max = [refs count];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
ref = [refs objectAtIndex: count];
|
||||
folder = [SOGoWebAppointmentFolder
|
||||
folderWithSubscriptionReference: ref
|
||||
inContainer: self];
|
||||
if (folder
|
||||
&& (forceReload || [folder reloadOnLogin]))
|
||||
[folder loadWebCalendar];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _migrateWebCalendarsSettings
|
||||
{
|
||||
SOGoUserSettings *us;
|
||||
@@ -474,6 +448,32 @@ static SoSecurityManager *sm = nil;
|
||||
[us synchronize];
|
||||
}
|
||||
|
||||
- (void) reloadWebCalendars: (BOOL) forceReload
|
||||
{
|
||||
NSArray *refs;
|
||||
SOGoWebAppointmentFolder *folder;
|
||||
SOGoUserSettings *us;
|
||||
NSDictionary *calSettings;
|
||||
NSString *ref;
|
||||
int count, max;
|
||||
|
||||
[self _migrateWebCalendarsSettings];
|
||||
us = [[SOGoUser userWithLogin: owner] userSettings];
|
||||
calSettings = [us objectForKey: @"Calendar"];
|
||||
refs = [[calSettings objectForKey: @"WebCalendars"] allKeys];
|
||||
max = [refs count];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
ref = [refs objectAtIndex: count];
|
||||
folder = [SOGoWebAppointmentFolder
|
||||
folderWithSubscriptionReference: ref
|
||||
inContainer: self];
|
||||
if (folder
|
||||
&& (forceReload || [folder reloadOnLogin]))
|
||||
[folder loadWebCalendar];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSException *) _fetchPersonalFolders: (NSString *) sql
|
||||
withChannel: (EOAdaptorChannel *) fc
|
||||
{
|
||||
|
||||
@@ -328,17 +328,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _requireResponseFromAttendees: (NSArray *) attendees
|
||||
- (void) _removeDelegationChain: (iCalPerson *) delegate
|
||||
inEvent: (iCalEvent *) event
|
||||
{
|
||||
NSEnumerator *enumerator;
|
||||
iCalPerson *currentAttendee;
|
||||
NSString *delegatedTo, *mailTo;
|
||||
|
||||
enumerator = [attendees objectEnumerator];
|
||||
while ((currentAttendee = [enumerator nextObject]))
|
||||
delegatedTo = [delegate delegatedTo];
|
||||
if ([delegatedTo length] > 0)
|
||||
{
|
||||
mailTo = [delegatedTo rfc822Email];
|
||||
delegate = [event findAttendeeWithEmail: mailTo];
|
||||
if (delegate)
|
||||
{
|
||||
[self _removeDelegationChain: delegate
|
||||
inEvent: event];
|
||||
[event removeFromAttendees: delegate];
|
||||
}
|
||||
else
|
||||
[self errorWithFormat:
|
||||
@"broken chain: delegate with email '%@' was not found",
|
||||
mailTo];
|
||||
}
|
||||
}
|
||||
|
||||
/* This method returns YES when any attendee has been removed and NO
|
||||
otherwise. */
|
||||
- (BOOL) _requireResponseFromAttendees: (iCalEvent *) event
|
||||
{
|
||||
NSArray *attendees;
|
||||
iCalPerson *currentAttendee;
|
||||
BOOL listHasChanged;
|
||||
int count, max;
|
||||
|
||||
attendees = [event attendees];
|
||||
max = [attendees count];
|
||||
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
currentAttendee = [attendees objectAtIndex: count];
|
||||
if ([[currentAttendee delegatedTo] length] > 0)
|
||||
{
|
||||
[self _removeDelegationChain: currentAttendee
|
||||
inEvent: event];
|
||||
[currentAttendee setDelegatedTo: nil];
|
||||
listHasChanged = YES;
|
||||
}
|
||||
[currentAttendee setRsvp: @"TRUE"];
|
||||
[currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction];
|
||||
}
|
||||
|
||||
return listHasChanged;
|
||||
}
|
||||
|
||||
- (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent
|
||||
@@ -400,6 +439,13 @@
|
||||
iCalEventChanges *changes;
|
||||
|
||||
changes = [newEvent getChangesRelativeToEvent: oldEvent];
|
||||
if ([changes sequenceShouldBeIncreased])
|
||||
{
|
||||
// Set new attendees status to "needs action" and recompute changes when
|
||||
// the list of attendees has changed.
|
||||
if ([self _requireResponseFromAttendees: newEvent])
|
||||
changes = [newEvent getChangesRelativeToEvent: oldEvent];
|
||||
}
|
||||
attendees = [changes deletedAttendees];
|
||||
if ([attendees count])
|
||||
{
|
||||
@@ -417,8 +463,6 @@
|
||||
if ([changes sequenceShouldBeIncreased])
|
||||
{
|
||||
[newEvent increaseSequence];
|
||||
// Set new attendees status to "needs action"
|
||||
[self _requireResponseFromAttendees: [newEvent attendees]];
|
||||
// Update attendees calendars and send them an update
|
||||
// notification by email
|
||||
[self _handleSequenceUpdateInEvent: newEvent
|
||||
@@ -455,10 +499,6 @@
|
||||
|
||||
if ([attendees count])
|
||||
{
|
||||
NSArray *originalAttendees;
|
||||
|
||||
originalAttendees = [NSArray arrayWithArray: [newEvent attendees]];
|
||||
|
||||
// Send an invitation to new attendees
|
||||
[self _handleAddedUsers: attendees fromEvent: newEvent];
|
||||
[self sendEMailUsingTemplateNamed: @"Invitation"
|
||||
|
||||
@@ -19,6 +19,7 @@ Contacts_OBJC_FILES = \
|
||||
SOGoContactLDIFEntry.m \
|
||||
SOGoContactSourceFolder.m \
|
||||
SOGoUserFolder+Contacts.m \
|
||||
SOGoContactEntryPhoto.m \
|
||||
|
||||
Contacts_RESOURCE_FILES += \
|
||||
Version \
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/* SOGoContactEntryPhoto.h - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifndef SOGOCONTACTENTRYPHOTO_H
|
||||
#define SOGOCONTACTENTRYPHOTO_H
|
||||
|
||||
#import <SOGo/SOGoObject.h>
|
||||
|
||||
@interface SOGoContactEntryPhoto : SOGoObject
|
||||
{
|
||||
int photoID;
|
||||
}
|
||||
|
||||
+ (id) entryPhotoWithID: (int) photoId
|
||||
inContainer: (id) container;
|
||||
|
||||
- (void) setPhotoID: (int) newPhotoID;
|
||||
|
||||
- (NSString *) davContentType;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOCONTACTENTRYPHOTO_H */
|
||||
@@ -0,0 +1,114 @@
|
||||
/* SOGoContactEntryPhoto.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import <Foundation/NSArray.h>
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
#import <NGObjWeb/WOContext.h>
|
||||
#import <NGObjWeb/WOResponse.h>
|
||||
|
||||
#import <NGCards/NGVCard.h>
|
||||
#import <NGCards/NGVCardPhoto.h>
|
||||
|
||||
#import "SOGoContactObject.h"
|
||||
|
||||
#import "SOGoContactEntryPhoto.h"
|
||||
|
||||
@implementation SOGoContactEntryPhoto
|
||||
|
||||
+ (id) entryPhotoWithID: (int) photoID
|
||||
inContainer: (id) container
|
||||
{
|
||||
id photo;
|
||||
|
||||
photo
|
||||
= [super objectWithName: [NSString stringWithFormat: @"photo%d", photoID]
|
||||
inContainer: container];
|
||||
[photo setPhotoID: photoID];
|
||||
|
||||
return photo;
|
||||
}
|
||||
|
||||
- (void) setPhotoID: (int) newPhotoID
|
||||
{
|
||||
photoID = newPhotoID;
|
||||
}
|
||||
|
||||
- (NGVCardPhoto *) photo
|
||||
{
|
||||
NGVCardPhoto *photo;
|
||||
NSArray *photoElements;
|
||||
|
||||
photoElements = [[container vCard] childrenWithTag: @"photo"];
|
||||
if ([photoElements count] > photoID)
|
||||
photo = [photoElements objectAtIndex: photoID];
|
||||
else
|
||||
photo = nil;
|
||||
|
||||
return photo;
|
||||
}
|
||||
|
||||
- (id) GETAction: (WOContext *) localContext
|
||||
{
|
||||
NGVCardPhoto *photo;
|
||||
NSData *data;
|
||||
id response;
|
||||
|
||||
photo = [self photo];
|
||||
if ([photo isInline])
|
||||
data = [photo decodedContent];
|
||||
else
|
||||
data = [[photo value: 0] dataUsingEncoding: NSISOLatin1StringEncoding];
|
||||
if (data)
|
||||
{
|
||||
response = [localContext response];
|
||||
|
||||
[response setHeader: [self davContentType] forKey: @"content-type"];
|
||||
[response setHeader: [NSString stringWithFormat:@" %d",
|
||||
[data length]]
|
||||
forKey: @"content-length"];
|
||||
[response setContent: data];
|
||||
}
|
||||
else
|
||||
response = nil;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
- (NSString *) davContentType
|
||||
{
|
||||
NGVCardPhoto *photo;
|
||||
NSString *type, *contentType;
|
||||
|
||||
photo = [self photo];
|
||||
if ([photo isInline])
|
||||
{
|
||||
type = [[photo type] lowercaseString];
|
||||
contentType = [NSString stringWithFormat: @"image/%@", type];
|
||||
}
|
||||
else
|
||||
contentType = @"text/plain";
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -24,6 +24,8 @@
|
||||
|
||||
#import <NGCards/NGVCard.h>
|
||||
|
||||
#import "SOGoContactEntryPhoto.h"
|
||||
|
||||
#import "SOGoContactGCSEntry.h"
|
||||
|
||||
@implementation SOGoContactGCSEntry
|
||||
@@ -62,6 +64,31 @@
|
||||
|
||||
/* actions */
|
||||
|
||||
- (id) lookupName: (NSString *) lookupName
|
||||
inContext: (id) localContext
|
||||
acquire: (BOOL) acquire
|
||||
{
|
||||
id obj;
|
||||
int photoIndex;
|
||||
NSArray *photoElements;
|
||||
|
||||
if ([lookupName hasPrefix: @"photo"])
|
||||
{
|
||||
photoElements = [[self vCard] childrenWithTag: @"photo"];
|
||||
photoIndex = [[lookupName substringFromIndex: 5] intValue];
|
||||
if (photoIndex > -1 && photoIndex < [photoElements count])
|
||||
obj = [SOGoContactEntryPhoto entryPhotoWithID: photoIndex
|
||||
inContainer: self];
|
||||
else
|
||||
obj = nil;
|
||||
}
|
||||
else
|
||||
obj = [super lookupName: lookupName inContext: localContext
|
||||
acquire: acquire];
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
- (NSException *) copyToFolder: (SOGoGCSFolder *) newFolder
|
||||
{
|
||||
NGVCard *newCard;
|
||||
|
||||
@@ -372,7 +372,7 @@
|
||||
- (NSString *) davContentLength
|
||||
{
|
||||
return [NSString stringWithFormat: @"%u",
|
||||
[content lengthOfBytesUsingEncoding: NSUTF8StringEncoding]];
|
||||
[content lengthOfBytesUsingEncoding: NSISOLatin1StringEncoding]];
|
||||
}
|
||||
|
||||
// - (NSString *) davResourceType
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#import <NGObjWeb/NSException+HTTP.h>
|
||||
#import <NGObjWeb/SoClassSecurityInfo.h>
|
||||
#import <NGObjWeb/SoSecurityManager.h>
|
||||
#import <NGObjWeb/WOApplication.h>
|
||||
#import <NGObjWeb/WOContext+SoObjects.h>
|
||||
#import <NGObjWeb/WORequest.h>
|
||||
@@ -147,18 +148,27 @@
|
||||
NSMutableArray *folders;
|
||||
NSEnumerator *subfolders;
|
||||
SOGoFolder *currentFolder;
|
||||
NSString *folderName;
|
||||
NSString *folderName, *folderOwner;
|
||||
Class subfolderClass;
|
||||
NSMutableDictionary *currentDictionary;
|
||||
SoSecurityManager *securityManager;
|
||||
|
||||
folders = [NSMutableArray array];
|
||||
|
||||
folderOwner = [parentFolder ownerInContext: context];
|
||||
securityManager = [SoSecurityManager sharedSecurityManager];
|
||||
|
||||
subfolderClass = [[parentFolder class] subFolderClass];
|
||||
|
||||
subfolders = [[parentFolder subFolders] objectEnumerator];
|
||||
while ((currentFolder = [subfolders nextObject]))
|
||||
{
|
||||
if ([currentFolder isMemberOfClass: subfolderClass])
|
||||
if (![securityManager validatePermission: SOGoPerm_AccessObject
|
||||
onObject: currentFolder
|
||||
inContext: context]
|
||||
&& [[currentFolder ownerInContext: context]
|
||||
isEqualToString: folderOwner]
|
||||
&& [currentFolder isMemberOfClass: subfolderClass])
|
||||
{
|
||||
folderName = [NSString stringWithFormat: @"/%@/%@",
|
||||
[parentFolder nameInContainer],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* UIxFolderActions.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2007 Inverse inc.
|
||||
* Copyright (C) 2007-2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Address";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Other";
|
||||
|
||||
"Address Books" = "Addressbooks";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Kontakt";
|
||||
"Address" = "Adresa";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Ostatní";
|
||||
|
||||
"Address Books" = "Složky kontaktů";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contactpersoon";
|
||||
"Address" = "Adres";
|
||||
"Photos" = "Fotos";
|
||||
"Other" = "Overige";
|
||||
|
||||
"Address Books" = "Addressbooks";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Address";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Other";
|
||||
|
||||
"Address Books" = "Address Books";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Adresses";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Informations complémentaires";
|
||||
|
||||
"Address Books" = "Carnet d'adresses";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Kontakt";
|
||||
"Address" = "Adresse";
|
||||
"Photos" = "Fotos";
|
||||
"Other" = "Sonstiges";
|
||||
|
||||
"Address Books" = "Adressbücher";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Address";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Other";
|
||||
|
||||
"Address Books" = "Addressbooks";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contatto";
|
||||
"Address" = "Indirizzo";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Altro";
|
||||
|
||||
"Address Books" = "Rubrica";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* this file is in UTF-8 format! */
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Address";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Other";
|
||||
|
||||
"Addressbook" = "Адресная книга";
|
||||
"Addresses" = "Адреса";
|
||||
"Update" = "Обновить";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Contacto";
|
||||
"Address" = "Dirección";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Otros datos";
|
||||
|
||||
"Address Books" = "Libretas de direcciones";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Kontakt";
|
||||
"Address" = "Adress";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Annat";
|
||||
|
||||
"Address Books" = "Adressböcker";
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
NSString *preferredEmail;
|
||||
NSString *item;
|
||||
NGVCard *card;
|
||||
NSMutableArray *photosURL;
|
||||
NSMutableDictionary *snapshot; /* contains the values for editing */
|
||||
SOGoContactFolder *componentAddressBook;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSURL.h>
|
||||
#import <Foundation/NSEnumerator.h>
|
||||
|
||||
#import <NGObjWeb/NSException+HTTP.h>
|
||||
@@ -33,6 +34,7 @@
|
||||
#import <NGExtensions/NSNull+misc.h>
|
||||
|
||||
#import <NGCards/NGVCard.h>
|
||||
#import <NGCards/NGVCardPhoto.h>
|
||||
#import <NGCards/NSArray+NGCards.h>
|
||||
|
||||
#import <Contacts/SOGoContactFolder.h>
|
||||
@@ -51,6 +53,7 @@
|
||||
{
|
||||
snapshot = [[NSMutableDictionary alloc] initWithCapacity: 16];
|
||||
preferredEmail = nil;
|
||||
photosURL = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -60,6 +63,7 @@
|
||||
{
|
||||
[snapshot release];
|
||||
[preferredEmail release];
|
||||
[photosURL release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@@ -282,6 +286,8 @@
|
||||
|
||||
if (![ce hasAttribute: @"type" havingValue: aTypeToExclude])
|
||||
break;
|
||||
|
||||
value = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,7 +418,7 @@
|
||||
[self _setSnapshotValue: @"telephoneNumber"
|
||||
to: [self _simpleValueForType: @"work" inArray: elements excluding: @"fax"]];
|
||||
[self _setSnapshotValue: @"homeTelephoneNumber"
|
||||
to: [self _simpleValueForType: @"home" inArray: elements excluding: nil]];
|
||||
to: [self _simpleValueForType: @"home" inArray: elements excluding: @"fax"]];
|
||||
[self _setSnapshotValue: @"mobile"
|
||||
to: [self _simpleValueForType: @"cell" inArray: elements excluding: nil]];
|
||||
[self _setSnapshotValue: @"facsimileTelephoneNumber"
|
||||
@@ -571,6 +577,36 @@
|
||||
&& [super canCreateOrModify]);
|
||||
}
|
||||
|
||||
- (NSArray *) photosURL
|
||||
{
|
||||
NSArray *photoElements;
|
||||
NSURL *soURL;
|
||||
NSString *baseInlineURL, *photoURL;
|
||||
NGVCardPhoto *photo;
|
||||
int count, max;
|
||||
|
||||
if (!photosURL)
|
||||
{
|
||||
soURL = [[self clientObject] soURL];
|
||||
baseInlineURL = [soURL absoluteString];
|
||||
photoElements = [card childrenWithTag: @"photo"];
|
||||
max = [photoElements count];
|
||||
photosURL = [[NSMutableArray alloc] initWithCapacity: max];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
photo = [photoElements objectAtIndex: count];
|
||||
if ([photo isInline])
|
||||
photoURL = [NSString stringWithFormat: @"%@/photo%d",
|
||||
baseInlineURL, count];
|
||||
else
|
||||
photoURL = [photo value: 0];
|
||||
[photosURL addObject: photoURL];
|
||||
}
|
||||
}
|
||||
|
||||
return photosURL;
|
||||
}
|
||||
|
||||
- (CardElement *) _elementWithTag: (NSString *) tag
|
||||
ofType: (NSString *) type
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* UIxContactFoldersView.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2006-2009 Inverse inc.
|
||||
* Copyright (C) 2006-2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
@@ -246,42 +246,6 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray *) _subFoldersFromFolder: (SOGoParentFolder *) parentFolder
|
||||
{
|
||||
NSMutableArray *folders;
|
||||
NSEnumerator *subfolders;
|
||||
SOGoGCSFolder *subfolder;
|
||||
NSString *folderName;
|
||||
NSMutableDictionary *currentDictionary;
|
||||
SoSecurityManager *securityManager;
|
||||
|
||||
securityManager = [SoSecurityManager sharedSecurityManager];
|
||||
|
||||
folders = [NSMutableArray array];
|
||||
|
||||
subfolders = [[parentFolder subFolders] objectEnumerator];
|
||||
while ((subfolder = [subfolders nextObject]))
|
||||
{
|
||||
if (![securityManager validatePermission: SOGoPerm_AccessObject
|
||||
onObject: subfolder inContext: context])
|
||||
{
|
||||
folderName = [NSString stringWithFormat: @"/%@/%@",
|
||||
[parentFolder nameInContainer],
|
||||
[subfolder nameInContainer]];
|
||||
currentDictionary
|
||||
= [NSMutableDictionary dictionaryWithCapacity: 3];
|
||||
[currentDictionary setObject: [subfolder displayName]
|
||||
forKey: @"displayName"];
|
||||
[currentDictionary setObject: folderName forKey: @"name"];
|
||||
[currentDictionary setObject: [subfolder folderType]
|
||||
forKey: @"type"];
|
||||
[folders addObject: currentDictionary];
|
||||
}
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
- (void) checkDefaultModulePreference
|
||||
{
|
||||
SOGoUserDefaults *ud;
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
NSArray *phones;
|
||||
CardElement *homeAdr;
|
||||
CardElement *workAdr;
|
||||
NSMutableArray *photosURL;
|
||||
}
|
||||
|
||||
- (NSString *) fullName;
|
||||
|
||||
@@ -20,30 +20,40 @@
|
||||
02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import <Foundation/NSURL.h>
|
||||
|
||||
#import <NGObjWeb/NSException+HTTP.h>
|
||||
#import <NGObjWeb/WOResponse.h>
|
||||
#import <NGCards/NGVCard.h>
|
||||
#import <NGCards/NGVCardPhoto.h>
|
||||
#import <NGCards/CardElement.h>
|
||||
#import <NGCards/NSArray+NGCards.h>
|
||||
#import <NGExtensions/NSString+Ext.h>
|
||||
|
||||
#import <SoObjects/Contacts/SOGoContactObject.h>
|
||||
#import <Contacts/SOGoContactObject.h>
|
||||
|
||||
#import "UIxContactView.h"
|
||||
|
||||
@implementation UIxContactView
|
||||
|
||||
/* accessors */
|
||||
- (id) init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
photosURL = nil;
|
||||
}
|
||||
|
||||
- (NSString *)tabSelection {
|
||||
NSString *selection;
|
||||
|
||||
selection = [self queryParameterForKey:@"tab"];
|
||||
if (selection == nil)
|
||||
selection = @"attributes";
|
||||
return selection;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc
|
||||
{
|
||||
[photosURL release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
/* accessors */
|
||||
|
||||
- (NSString *) _cardStringWithLabel: (NSString *) label
|
||||
value: (NSString *) value
|
||||
{
|
||||
@@ -139,15 +149,29 @@
|
||||
// We might not have a preferred item but rather something like this:
|
||||
// EMAIL;TYPE=work:dd@ee.com
|
||||
// EMAIL;TYPE=home:ff@gg.com
|
||||
// In this case, we always return the last entry.
|
||||
//
|
||||
// or:
|
||||
//
|
||||
// EMAIL;TYPE=INTERNET:a@a.com
|
||||
// EMAIL;TYPE=INTERNET,HOME:b@b.com
|
||||
//
|
||||
// In this case, we always return the entry NOT matching the primaryEmail
|
||||
if ([emails count] > 0)
|
||||
{
|
||||
email = [[emails objectAtIndex: [emails count]-1] value: 0];
|
||||
int i;
|
||||
|
||||
if ([email caseInsensitiveCompare: [card preferredEMail]] != NSOrderedSame)
|
||||
mailTo = [NSString stringWithFormat: @"<a href=\"mailto:%@\""
|
||||
@" onclick=\"return openMailTo('%@ <%@>');\">"
|
||||
@"%@</a>", email, [[card fn] stringByReplacingString: @"\"" withString: @""], email, email];
|
||||
for (i = 0; i < [emails count]; i++)
|
||||
{
|
||||
email = [[emails objectAtIndex: i] value: 0];
|
||||
|
||||
if ([email caseInsensitiveCompare: [card preferredEMail]] != NSOrderedSame)
|
||||
{
|
||||
mailTo = [NSString stringWithFormat: @"<a href=\"mailto:%@\""
|
||||
@" onclick=\"return openMailTo('%@ <%@>');\">"
|
||||
@"%@</a>", email, [[card fn] stringByReplacingString: @"\"" withString: @""], email, email];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [self _cardStringWithLabel: @"Additional Email:"
|
||||
@@ -214,6 +238,8 @@
|
||||
|
||||
if (![ce hasAttribute: @"type" havingValue: aTypeToExclude])
|
||||
break;
|
||||
|
||||
phone = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +255,7 @@
|
||||
|
||||
- (NSString *) homePhone
|
||||
{
|
||||
return [self _phoneOfType: @"home" withLabel: @"Home:" excluding: nil];
|
||||
return [self _phoneOfType: @"home" withLabel: @"Home:" excluding: @"fax"];
|
||||
}
|
||||
|
||||
- (NSString *) fax
|
||||
@@ -614,4 +640,34 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray *) photosURL
|
||||
{
|
||||
NSArray *photoElements;
|
||||
NSURL *soURL;
|
||||
NSString *baseInlineURL, *photoURL;
|
||||
NGVCardPhoto *photo;
|
||||
int count, max;
|
||||
|
||||
if (!photosURL)
|
||||
{
|
||||
soURL = [[self clientObject] soURL];
|
||||
baseInlineURL = [soURL absoluteString];
|
||||
photoElements = [card childrenWithTag: @"photo"];
|
||||
max = [photoElements count];
|
||||
photosURL = [[NSMutableArray alloc] initWithCapacity: max];
|
||||
for (count = 0; count < max; count++)
|
||||
{
|
||||
photo = [photoElements objectAtIndex: count];
|
||||
if ([photo isInline])
|
||||
photoURL = [NSString stringWithFormat: @"%@/photo%d",
|
||||
baseInlineURL, count];
|
||||
else
|
||||
photoURL = [photo value: 0];
|
||||
[photosURL addObject: photoURL];
|
||||
}
|
||||
}
|
||||
|
||||
return photosURL;
|
||||
}
|
||||
|
||||
@end /* UIxContactView */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* UIxContactsUserFolders.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2007 Inverse inc.
|
||||
* Copyright (C) 2007-2010 Inverse inc.
|
||||
*
|
||||
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"Contact" = "Контакт";
|
||||
"Address" = "Адреса";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Інше";
|
||||
|
||||
"Address Books" = "Адресні книги";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* this file is in UTF-8 format! */
|
||||
|
||||
"Contact" = "Contact";
|
||||
"Address" = "Address";
|
||||
"Photos" = "Photos";
|
||||
"Other" = "Other";
|
||||
|
||||
"Addressbook" = "Llyfr cyfeiriadau";
|
||||
"Addresses" = "Cyfeiriadau";
|
||||
"Update" = "Diweddaru";
|
||||
|
||||
@@ -22,7 +22,7 @@ request_info = "vous invite à une réunion.";
|
||||
"Update status" = "Intégrer les modifications";
|
||||
Accept = "Accepter";
|
||||
Decline = "Decliner";
|
||||
Tentative = "Tentative";
|
||||
Tentative = "Tentatif";
|
||||
"Delegate ..." = "Déléguer ...";
|
||||
"Delegated to" = "Délégué à";
|
||||
"Update status in calendar" = "Mettre l'agenda à jour";
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Version file
|
||||
|
||||
SUBMINOR_VERSION:=2
|
||||
SUBMINOR_VERSION:=0
|
||||
|
||||
# v0.9.1 requires Main v0.9.59
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
/* status type */
|
||||
"status_" = "Non-spécifié";
|
||||
"status_NOT-SPECIFIED" = "Non spécifié";
|
||||
"status_TENTATIVE" = "Tentative";
|
||||
"status_TENTATIVE" = "Tentatif";
|
||||
"status_CONFIRMED" = "Confirmé";
|
||||
"status_CANCELLED" = "Annulé";
|
||||
"status_NEEDS-ACTION" = "En attente";
|
||||
@@ -470,7 +470,7 @@ validate_endbeforestart = "La date de fin est avant la date de début.";
|
||||
"Needs action" = "En attente";
|
||||
"Accepted" = "Accepté";
|
||||
"Declined" = "Décliné";
|
||||
"Tentative" = "Tentative";
|
||||
"Tentative" = "Tentatif";
|
||||
|
||||
"Free" = "Libre";
|
||||
"Busy" = "Occupé";
|
||||
|
||||
@@ -256,23 +256,22 @@ static NSArray *tasksFields = nil;
|
||||
static NSString *fields[] = { @"startDate", @"c_startdate",
|
||||
@"endDate", @"c_enddate" };
|
||||
|
||||
if (dayBasedView)
|
||||
for (count = 0; count < 2; count++)
|
||||
{
|
||||
aDateField = fields[count * 2];
|
||||
aDate = [aRecord objectForKey: aDateField];
|
||||
daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate]
|
||||
- [userTimeZone secondsFromGMTForDate: startDate]);
|
||||
if (daylightOffset)
|
||||
{
|
||||
aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0
|
||||
minutes: 0 seconds: daylightOffset];
|
||||
[aRecord setObject: aDate forKey: aDateField];
|
||||
aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]];
|
||||
[aRecord setObject: aDateValue forKey: fields[count * 2 + 1]];
|
||||
}
|
||||
}
|
||||
|
||||
for (count = 0; count < 2; count++)
|
||||
{
|
||||
aDateField = fields[count * 2];
|
||||
aDate = [aRecord objectForKey: aDateField];
|
||||
daylightOffset = (int) ([userTimeZone secondsFromGMTForDate: aDate]
|
||||
- [userTimeZone secondsFromGMTForDate: startDate]);
|
||||
if (daylightOffset)
|
||||
{
|
||||
aDate = [aDate dateByAddingYears: 0 months: 0 days: 0 hours: 0
|
||||
minutes: 0 seconds: daylightOffset];
|
||||
[aRecord setObject: aDate forKey: aDateField];
|
||||
aDateValue = [NSNumber numberWithInt: [aDate timeIntervalSince1970]];
|
||||
[aRecord setObject: aDateValue forKey: fields[count * 2 + 1]];
|
||||
}
|
||||
}
|
||||
|
||||
aDateValue = [aRecord objectForKey: @"c_recurrence_id"];
|
||||
aDate = [aRecord objectForKey: @"cycleStartDate"];
|
||||
if (aDateValue && aDate)
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
</li>
|
||||
<li target="addressesInfos">
|
||||
<span><var:string label:value="Address" /></span></li>
|
||||
<li target="photos">
|
||||
<span><var:string label:value="Photos" /></span></li>
|
||||
<li target="otherInfos">
|
||||
<span><var:string label:value="Other" /></span></li>
|
||||
</ul>
|
||||
@@ -332,6 +334,12 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="photos" class="tab">
|
||||
<var:foreach list="photosURL" item="currentPhotoURL">
|
||||
<img var:src="currentPhotoURL" class="contactPhoto"/><br
|
||||
/></var:foreach>
|
||||
</div>
|
||||
|
||||
<div id="otherInfos" class="tab">
|
||||
<table class="framenocaption">
|
||||
<tr>
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
/><var:string value="secondaryEmail" escapeHTML="NO"
|
||||
/><var:string value="screenName" escapeHTML="NO"
|
||||
/><var:string value="preferredAddress" escapeHTML="NO"
|
||||
/></div
|
||||
|
||||
/><var:foreach list="photosURL" item="currentPhotoURL"
|
||||
><br/><img var:src="currentPhotoURL" class="contactPhoto"/>
|
||||
</var:foreach></div
|
||||
|
||||
><var:if condition="hasHomeInfos"
|
||||
><div id="homeInfos"
|
||||
><h4><var:string label:value="Home" /></h4
|
||||
|
||||
@@ -1272,8 +1272,9 @@ function startDragging (itm, e) {
|
||||
|
||||
handle.show();
|
||||
handle.update (count);
|
||||
if (e.shiftKey || currentFolderIsRemote ())
|
||||
if (e.shiftKey || currentFolderIsRemote ()) {
|
||||
handle.addClassName ("copy");
|
||||
}
|
||||
}
|
||||
|
||||
function whileDragging (itm, e) {
|
||||
@@ -1311,7 +1312,8 @@ function dropSelectedContacts (action, toId) {
|
||||
}
|
||||
}
|
||||
var fromId = $(selectedFolders[0]).id;
|
||||
if (!currentFolderIsRemote () || action != "move") {
|
||||
if ((!currentFolderIsRemote () || action != "move")
|
||||
&& fromId.substring(1) != toId) {
|
||||
var url = ApplicationBaseURL + fromId + "/" + action
|
||||
+ "?folder=" + toId + "&uid="
|
||||
+ contactIds.join("&uid=");
|
||||
|
||||
@@ -448,7 +448,8 @@ function deleteMessageWithDelay(url, id, mailbox, messageId) {
|
||||
}
|
||||
|
||||
function onPrintCurrentMessage(event) {
|
||||
var rowIds = $("messageList").getSelectedRowsId();
|
||||
var messageList = $("messageListBody").down("TBODY");
|
||||
var rowIds = messageList.getSelectedNodes();
|
||||
if (rowIds.length == 0) {
|
||||
window.alert(_("Please select a message to print."));
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# of the executable.
|
||||
|
||||
MAJOR_VERSION=1
|
||||
MINOR_VERSION=2
|
||||
MINOR_VERSION=3
|
||||
|
||||
@@ -188,7 +188,7 @@ rm -fr ${RPM_BUILD_ROOT}
|
||||
|
||||
%config %{_sysconfdir}/httpd/conf.d/SOGo.conf
|
||||
%config %{_sysconfdir}/sysconfig/sogo
|
||||
%doc ChangeLog README NEWS Scripts/sql-update-20070724.sh Scripts/sql-update-20070822.sh Scripts/sql-update-20080303.sh Scripts/sql-update-101_to_102.sh Scripts/sql-update-1.2.2_to_1.3.0.sh sql-update-1.2.2_to_1.3.0-mysql.sh
|
||||
%doc ChangeLog README NEWS Scripts/sql-update-20070724.sh Scripts/sql-update-20070822.sh Scripts/sql-update-20080303.sh Scripts/sql-update-101_to_102.sh Scripts/sql-update-1.2.2_to_1.3.0.sh Scripts/sql-update-1.2.2_to_1.3.0-mysql.sh
|
||||
|
||||
%files -n sogo-tool
|
||||
%{prefix}/Tools/Admin/sogo-tool
|
||||
|
||||
Reference in New Issue
Block a user