From d6888ab037bcfbc5255e9d8a7fdc9b1edfbb7463 Mon Sep 17 00:00:00 2001 From: wolfgang Date: Mon, 26 Nov 2007 23:19:01 +0000 Subject: [PATCH] git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1277 d1b88da0-ebda-0310-925b-ed51d893ca5b --- ChangeLog | 89 ++++++++++ SOPE/NGCards/ChangeLog | 9 + SOPE/NGCards/NGVCard.m | 17 +- SOPE/NGCards/iCalCalendar.m | 7 + .../Appointments/SOGoAppointmentFolder.h | 5 +- .../Appointments/SOGoAppointmentFolder.m | 100 ++++++++++-- SoObjects/Contacts/SOGoContactGCSFolder.m | 2 +- SoObjects/Mailer/SOGoDraftObject.m | 11 ++ SoObjects/Mailer/SOGoMailForward.m | 4 +- SoObjects/Mailer/SOGoMailObject.m | 16 +- SoObjects/Mailer/SOGoMailReply.m | 18 +- SoObjects/SOGo/LDAPSource.h | 2 + SoObjects/SOGo/LDAPSource.m | 56 ++++++- SoObjects/SOGo/LDAPUserManager.m | 16 +- SoObjects/SOGo/SOGoUser.h | 3 + SoObjects/SOGo/SOGoUser.m | 11 ++ SoObjects/SOGo/SOGoUserFolder.m | 26 ++- UI/Common/UIxPageFrame.m | 18 ++ UI/Common/WODirectAction+SOGo.h | 2 + UI/Common/WODirectAction+SOGo.m | 34 +++- .../UIxMailPartAlternativeViewer.m | 2 + UI/MailPartViewers/UIxMailPartICalViewer.m | 18 -- UI/MailPartViewers/UIxMailPartViewer.m | 43 +++-- UI/MailerUI/English.lproj/Localizable.strings | 3 + UI/MailerUI/French.lproj/Localizable.strings | 3 + UI/MailerUI/German.lproj/Localizable.strings | 3 + UI/MailerUI/UIxMailAccountActions.m | 2 +- UI/MailerUI/UIxMailEditor.m | 2 +- UI/PreferencesUI/UIxPreferences.m | 12 +- UI/SOGoUI/UIxComponent.m | 6 - .../English.lproj/Localizable.strings | 8 + UI/Scheduler/French.lproj/Localizable.strings | 8 + UI/Scheduler/German.lproj/Localizable.strings | 9 + UI/Scheduler/UIxAppointmentEditor.m | 6 +- UI/Scheduler/UIxCalListingActions.h | 3 +- UI/Scheduler/UIxCalListingActions.m | 48 +++--- UI/Scheduler/UIxTaskEditor.m | 16 +- UI/Templates/MailerUI/UIxMailMainFrame.wox | 21 +++ UI/Templates/PreferencesUI/UIxPreferences.wox | 110 +++++++------ UI/Templates/UIxPageFrame.wox | 27 ++- UI/WebServerResources/MailerUI.js | 154 +++++++++++++----- UI/WebServerResources/SchedulerUI.js | 6 +- .../UIxAppointmentEditor.css | 2 +- UI/WebServerResources/UIxMailEditor.js | 4 +- UI/WebServerResources/generic.css | 5 +- UI/WebServerResources/generic.js | 47 ++++-- 46 files changed, 750 insertions(+), 264 deletions(-) diff --git a/ChangeLog b/ChangeLog index f3a0e7f0..58209d70 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,92 @@ +2007-11-26 Wolfgang Sourdeau + + * UI/PreferencesUI/UIxPreferences.m ([UIxPreferences -userHasCalendarAccess]) + ([UIxPreferences -userHasMailAccess]): same as below, for + displaying preference tabs. + + * UI/Common/UIxPageFrame.m ([UIxPageFrame + -userHasCalendarAccess]): new accessor for the link banner. + ([UIxPageFrame -userHasMailAccess]): same as above. + + * SoObjects/SOGo/SOGoUserFolder.m ([SOGoUserFolder + -toManyRelationshipKeys]): do not report the path to modules to + which the user has no access. No longer cache this information + statically, the array will be generated at each call. + ([SOGoUserFolder -lookupName:_keyinContext:_ctxacquire:_flag]): + ignore the path to modules to which the user has no access. + + * SoObjects/SOGo/SOGoUser.m ([SOGoUser -canAccessModule:module]): + new method that returns whether the user has access to the + specified module. + + * SoObjects/SOGo/LDAPSource.m ([LDAPSource + -initFromUDSource:udSource]): take a new parameter named + 'ModulesContraints' that defines a set of constraints for + accessing specified named modules. This is an optout, meaning the + modules will be present unless a constraint is specified. + + * SoObjects/Appointments/SOGoAppointmentFolder.m + ([SOGoAppointmentFolder + -roleForComponentsWithAccessClass:accessClassforUser:uid]): cache + acl to reduce method calls. + ([SOGoAppointmentFolder + -fetchFields:_fieldsfromFolder:_folderfrom:_startDateto:_endDatetitle:titlecomponent:_component]): + if the user is not the current user, we strip the useless info + from the meta data. + + * UI/Common/WODirectAction+SOGo.m ([WODirectAction + -labelForKey:key]): new category method. + + * UI/Scheduler/UIxAppointmentEditor.m ([UIxAppointmentEditor + -event]): same as below. + + * UI/Scheduler/UIxTaskEditor.m ([UIxTaskEditor -todo]): request a + secured version of the component. + + * UI/MailPartViewers/UIxMailPartViewer.m + ([-fallbackStringEncoding]): removed method. + ([UIxMailPartViewer -flatContentAsString]): we no longer use [self + fallbackStringEncoding]. Instead we directly specify + NSISOLatin1StringEncoding if UTF-8 has failed. + + * UI/MailPartViewers/UIxMailPartICalViewer.m + ([-fallbackStringEncoding]): removed method. + +2007-11-25 Ludovic Marcotte + + * SoObjects/Mailer/SOGoMailForward.m + Use [sourceMail decodedSubject] instead of [sourceMail subject] + Signature fix in -signature - see the comment + for SOGoMailReply. + + * SoObjects/Mailer/SOGoMailObject.m + Improved -stringFromData: to try UTF-8 then fallback to Latin1 + + * SoObjects/Mailer/SOGoMailReply.m + Modified -messageBody to strip the signature from the reply. + Also modified -signature to add "-- \n%@" instead of the + broken ""--\r\n%@" pattern. + + * UI/MailPartViewers/UIxMailPartAlternativeViewer.m + We now favor text/calendar parts over text/html and + text/plain parts when viewing a multipart/alternative mail. + This allows us to show the email invitations coming from + Microsoft Outlook. + + * UI/MailerUI/UIxMailAccountActions.m + Signature fix in -composeAction - see the comment + for SOGoMailReply. + + * UI/MailPartViewers/UIxMailPartViewer.m + Modified -flatContentAsString to use UTF-8 as the + default fallback encoding for 8-bit content. + + * SoObjects/Mailer/SOGoDraftObject.m + Modified _fillInReplyAddresses:replyToAll:envelope: + so that if there's no recipient, we add at least + ourself to the list. + Fixed a mem leak in the same method. + 2007-11-22 Wolfgang Sourdeau * UI/MailPartViewers/UIxMailRenderingContext.m diff --git a/SOPE/NGCards/ChangeLog b/SOPE/NGCards/ChangeLog index 75c6d547..e13917d6 100644 --- a/SOPE/NGCards/ChangeLog +++ b/SOPE/NGCards/ChangeLog @@ -1,3 +1,12 @@ +2007-11-26 Wolfgang Sourdeau + + * iCalCalendar.m ([iCalCalendar -versitString]): same as below, + with "2.0" as value. + + * NGVCard.m ([NGVCard -versitString]): overriden method by forcing + version to "3.0" since it's the format we comply with. + + 2007-11-22 Wolfgang Sourdeau * iCalEntityObject.m ([iCalEntityObject -compare:otherObject]): diff --git a/SOPE/NGCards/NGVCard.m b/SOPE/NGCards/NGVCard.m index faf7eaa5..cf457c4e 100644 --- a/SOPE/NGCards/NGVCard.m +++ b/SOPE/NGCards/NGVCard.m @@ -29,16 +29,6 @@ @implementation NGVCard -+ (id) parseSingleFromSource: (id) source -{ - NGVCard *newCard; - - newCard = [super parseSingleFromSource: source]; - [newCard setVersion: @"3.0"]; - - return newCard; -} - + (id) cardWithUid: (NSString *) _uid { NGVCard *newCard; @@ -483,6 +473,13 @@ return [self _preferredElementWithTag: @"adr"]; } +- (NSString *) versitString +{ + [self setVersion: @"3.0"]; + + return [super versitString]; +} + /* description */ - (void) appendAttributesToDescription: (NSMutableString *) _ms diff --git a/SOPE/NGCards/iCalCalendar.m b/SOPE/NGCards/iCalCalendar.m index b478b7f9..1771c65d 100644 --- a/SOPE/NGCards/iCalCalendar.m +++ b/SOPE/NGCards/iCalCalendar.m @@ -200,6 +200,13 @@ return ma; } +- (NSString *) versitString +{ + [self setVersion: @"2.0"]; + + return [super versitString]; +} + /* ical typing */ - (NSString *) entityName diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.h b/SoObjects/Appointments/SOGoAppointmentFolder.h index 851b4855..c359f65e 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.h +++ b/SoObjects/Appointments/SOGoAppointmentFolder.h @@ -38,6 +38,8 @@ #import "SOGo/SOGoGCSFolder.h" +#import + @class NSArray; @class NSCalendarDate; @class NSException; @@ -46,11 +48,12 @@ @class NSTimeZone; @class GCSFolder; -#import @interface SOGoAppointmentFolder : SOGoGCSFolder { NSTimeZone *timeZone; NSMutableDictionary *uidToFilename; + NSMutableDictionary *aclMatrix; + NSMutableArray *stripFields; } - (BOOL) isActive; diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index 9d740ed7..cafd2173 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -35,6 +35,7 @@ #import #import #import +#import #import #import #import @@ -110,6 +111,9 @@ static NSNumber *sharedYes = nil; if ((self = [super initWithName: name inContainer: newContainer])) { timeZone = [[context activeUser] timeZone]; + aclMatrix = [NSMutableDictionary new]; + stripFields = nil; + uidToFilename = nil; } return self; @@ -117,6 +121,8 @@ static NSNumber *sharedYes = nil; - (void) dealloc { + [aclMatrix release]; + [stripFields release]; [uidToFilename release]; [super dealloc]; } @@ -765,7 +771,7 @@ static NSNumber *sharedYes = nil; [self _privacyClassificationStringsForUID: login], email, email, email]; } - + return privacySqlString; } @@ -774,6 +780,7 @@ static NSNumber *sharedYes = nil; { NSString *accessRole, *prefix, *currentRole, *suffix; NSEnumerator *acls; + NSMutableDictionary *userRoles; accessRole = nil; @@ -784,20 +791,75 @@ static NSNumber *sharedYes = nil; else prefix = @"Confidential"; - acls = [[self aclsForUser: uid] objectEnumerator]; - currentRole = [acls nextObject]; - while (currentRole && !accessRole) - if ([currentRole hasPrefix: prefix]) - { - suffix = [currentRole substringFromIndex: [prefix length]]; - accessRole = [NSString stringWithFormat: @"Component%@", suffix]; - } - else + userRoles = [aclMatrix objectForKey: uid]; + if (!userRoles) + { + userRoles = [NSMutableDictionary dictionaryWithCapacity: 3]; + [aclMatrix setObject: userRoles forKey: uid]; + } + + accessRole = [userRoles objectForKey: prefix]; + if (!accessRole) + { + acls = [[self aclsForUser: uid] objectEnumerator]; currentRole = [acls nextObject]; + while (currentRole && !accessRole) + if ([currentRole hasPrefix: prefix]) + { + suffix = [currentRole substringFromIndex: [prefix length]]; + accessRole = [NSString stringWithFormat: @"Component%@", suffix]; + } + else + currentRole = [acls nextObject]; + if (!accessRole) + accessRole = @""; + [userRoles setObject: accessRole forKey: prefix]; + } return accessRole; } +- (void) _buildStripFieldsFromFields: (NSArray *) fields +{ + stripFields = [[NSMutableArray alloc] initWithCapacity: [fields count]]; + [stripFields setArray: fields]; + [stripFields removeObjectsInArray: [NSArray arrayWithObjects: @"c_name", + @"c_uid", @"c_startdate", + @"c_enddate", @"c_isallday", + @"c_iscycle", + @"c_classification", + @"c_component", nil]]; +} + +- (void) _fixupProtectedInformation: (NSEnumerator *) ma + inFields: (NSArray *) fields + forUser: (NSString *) uid +{ + NSMutableDictionary *currentRecord; + NSString *roles[] = {nil, nil, nil}; + iCalAccessClass accessClass; + NSString *role; + + if (!stripFields) + [self _buildStripFieldsFromFields: fields]; + +#warning we do not take the participation status into account + while ((currentRecord = [ma nextObject])) + { + accessClass + = [[currentRecord objectForKey: @"c_classification"] intValue]; + role = roles[accessClass]; + if (!role) + { + role = [[self roleForComponentsWithAccessClass: accessClass + forUser: uid] substringFromIndex: 9]; + roles[accessClass] = role; + } + if ([role isEqualToString: @"DAndTViewer"]) + [currentRecord removeObjectsForKeys: stripFields]; + } +} + - (NSArray *) fetchFields: (NSArray *) _fields fromFolder: (GCSFolder *) _folder from: (NSCalendarDate *) _startDate @@ -809,15 +871,16 @@ static NSNumber *sharedYes = nil; NSMutableArray *fields, *ma = nil; NSArray *records; NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString, - *privacySqlString; + *privacySqlString, *currentLogin; NGCalendarDateRange *r; - if (_folder == nil) { - [self errorWithFormat:@"(%s): missing folder for fetch!", + if (!_folder) + { + [self errorWithFormat:@"(%s): missing folder for fetch!", __PRETTY_FUNCTION__]; - return nil; - } - + return nil; + } + if (_startDate && _endDate) { r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate @@ -896,6 +959,11 @@ static NSNumber *sharedYes = nil; if (logger) [self debugWithFormat:@"returning %i records", [ma count]]; + currentLogin = [[context activeUser] login]; + if (![currentLogin isEqualToString: owner]) + [self _fixupProtectedInformation: [ma objectEnumerator] + inFields: _fields + forUser: currentLogin]; // [ma makeObjectsPerform: @selector (setObject:forKey:) // withObject: owner // withObject: @"owner"]; diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 7fd56ba8..074048b8 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -106,7 +106,7 @@ qs = [NSString stringWithFormat: @"(c_sn isCaseInsensitiveLike: '%@%%') OR " @"(c_givenname isCaseInsensitiveLike: '%@%%') OR " - @"(c_mail isCaseInsensitiveLike: '%@%%') OR " + @"(c_mail isCaseInsensitiveLike: '%%%@%%') OR " @"(c_telephonenumber isCaseInsensitiveLike: '%%%@%%')", filter, filter, filter, filter]; qualifier = [EOQualifier qualifierWithQualifierFormat: qs]; diff --git a/SoObjects/Mailer/SOGoDraftObject.m b/SoObjects/Mailer/SOGoDraftObject.m index f56c1be8..0ebb3432 100644 --- a/SoObjects/Mailer/SOGoDraftObject.m +++ b/SoObjects/Mailer/SOGoDraftObject.m @@ -488,7 +488,18 @@ static BOOL showTextAttachmentsInline = NO; [to release]; } + /* If "to" is empty, we add at least ourself as a recipient! + This is for emails in the "Sent" folder that we reply to... */ + if (![to count]) + { + if ([[_envelope replyTo] count]) + [self _addEMailsOfAddresses: [_envelope replyTo] toArray: to]; + else + [self _addEMailsOfAddresses: [_envelope from] toArray: to]; + } + [allRecipients release]; + [addrs release]; } - (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths diff --git a/SoObjects/Mailer/SOGoMailForward.m b/SoObjects/Mailer/SOGoMailForward.m index e666f9b8..37bce7a5 100644 --- a/SoObjects/Mailer/SOGoMailForward.m +++ b/SoObjects/Mailer/SOGoMailForward.m @@ -55,7 +55,7 @@ - (NSString *) subject { - return [sourceMail subject]; + return [sourceMail decodedSubject]; } - (NSString *) date @@ -155,7 +155,7 @@ signature = [[context activeUser] signature]; if ([signature length]) - mailSignature = [NSString stringWithFormat: @"--\r\n%@", signature]; + mailSignature = [NSString stringWithFormat: @"-- \n%@", signature]; else mailSignature = @""; diff --git a/SoObjects/Mailer/SOGoMailObject.m b/SoObjects/Mailer/SOGoMailObject.m index f59ab0f0..a28d752e 100644 --- a/SoObjects/Mailer/SOGoMailObject.m +++ b/SoObjects/Mailer/SOGoMailObject.m @@ -659,17 +659,21 @@ static BOOL debugSoParts = NO; charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"]; if (![charset length]) { - s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding]; - [s autorelease]; + s = nil; } else { s = [NSString stringWithData: mailData usingEncodingNamed: charset]; - - // If it has failed, we try at least using UTF-8. Normally, this can NOT fail - if (!s) - s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease]; } + + // If it has failed, we try at least using UTF-8. Normally, this can NOT fail. + // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's + // the case + if (!s) + s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease]; + + if (!s) + s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease]; } else s = nil; diff --git a/SoObjects/Mailer/SOGoMailReply.m b/SoObjects/Mailer/SOGoMailReply.m index 9016b469..f196b33c 100644 --- a/SoObjects/Mailer/SOGoMailReply.m +++ b/SoObjects/Mailer/SOGoMailReply.m @@ -70,7 +70,21 @@ - (NSString *) messageBody { - return [[sourceMail contentForEditing] stringByApplyingMailQuoting]; + NSString *s; + + s = [sourceMail contentForEditing]; + + if (s) + { + NSRange r; + + r = [s rangeOfString: @"\n-- \n" options: NSBackwardsSearch]; + + if (r.length) + s = [s substringToIndex: r.location]; + } + + return [s stringByApplyingMailQuoting]; } - (NSString *) signature @@ -79,7 +93,7 @@ signature = [[context activeUser] signature]; if ([signature length]) - mailSignature = [NSString stringWithFormat: @"--\r\n%@", signature]; + mailSignature = [NSString stringWithFormat: @"-- \n%@", signature]; else mailSignature = @""; diff --git a/SoObjects/SOGo/LDAPSource.h b/SoObjects/SOGo/LDAPSource.h index c2edec3d..2124b609 100644 --- a/SoObjects/SOGo/LDAPSource.h +++ b/SoObjects/SOGo/LDAPSource.h @@ -44,6 +44,8 @@ NSArray *mailFields; NSString *bindFields; + NSDictionary *modulesConstraints; + NGLdapConnection *ldapConnection; NSMutableArray *searchAttributes; } diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index b2cd7ad3..f3239afb 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -170,6 +170,7 @@ static int sizeLimit; [bindFields release]; [ldapConnection release]; [sourceID release]; + [modulesConstraints release]; [super dealloc]; } @@ -189,6 +190,7 @@ static int sizeLimit; UIDField: [udSource objectForKey: @"UIDFieldName"] mailFields: [udSource objectForKey: @"MailFieldNames"] andBindFields: [udSource objectForKey: @"bindFields"]]; + ASSIGN (modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]); return self; } @@ -351,6 +353,20 @@ static int sizeLimit; return [EOQualifier qualifierWithQualifierFormat: qs]; } +- (NSArray *) _contraintsFields +{ + NSMutableArray *fields; + NSEnumerator *values; + NSDictionary *currentConstraint; + + fields = [NSMutableArray array]; + values = [[modulesConstraints allValues] objectEnumerator]; + while ((currentConstraint = [values nextObject])) + [fields addObjectsFromArray: [currentConstraint allKeys]]; + + return fields; +} + - (NSArray *) _searchAttributes { if (!searchAttributes) @@ -361,6 +377,7 @@ static int sizeLimit; if (UIDField) [searchAttributes addObject: UIDField]; [searchAttributes addObjectsFromArray: mailFields]; + [searchAttributes addObjectsFromArray: [self _contraintsFields]]; [searchAttributes addObjectsFromArray: commonSearchFields]; } @@ -408,7 +425,8 @@ static int sizeLimit; emailFields = [mailFields objectEnumerator]; while ((currentFieldName = [emailFields nextObject])) { - value = [[ldapEntry attributeWithName: currentFieldName] stringValueAtIndex: 0]; + value = [[ldapEntry attributeWithName: currentFieldName] + stringValueAtIndex: 0]; if (value) [emails addObject: value]; } @@ -416,6 +434,38 @@ static int sizeLimit; [contactEntry setObject: emails forKey: @"c_emails"]; } +- (void) _fillConstraints: (NGLdapEntry *) ldapEntry + forModule: (NSString *) module + intoContactEntry: (NSMutableDictionary *) contactEntry +{ + NSDictionary *constraints; + NSEnumerator *matches; + NSString *currentMatch, *currentValue, *ldapValue; + BOOL result; + + result = YES; + + constraints = [modulesConstraints objectForKey: module]; + if (constraints) + { + matches = [[constraints allKeys] objectEnumerator]; + currentMatch = [matches nextObject]; + while (result && currentMatch) + { + ldapValue = [[ldapEntry attributeWithName: currentMatch] + stringValueAtIndex: 0]; + currentValue = [constraints objectForKey: currentMatch]; + if ([ldapValue isEqualToString: currentValue]) + currentMatch = [matches nextObject]; + else + result = NO; + } + } + + [contactEntry setObject: [NSNumber numberWithBool: result] + forKey: [NSString stringWithFormat: @"%@Access", module]]; +} + - (NSDictionary *) _convertLDAPEntryToContact: (NGLdapEntry *) ldapEntry { NSMutableDictionary *contactEntry; @@ -446,6 +496,10 @@ static int sizeLimit; value = @""; [contactEntry setObject: value forKey: @"c_cn"]; [self _fillEmailsOfEntry: ldapEntry intoContactEntry: contactEntry]; + [self _fillConstraints: ldapEntry forModule: @"Calendar" + intoContactEntry: (NSMutableDictionary *) contactEntry]; + [self _fillConstraints: ldapEntry forModule: @"Mail" + intoContactEntry: (NSMutableDictionary *) contactEntry]; return contactEntry; } diff --git a/SoObjects/SOGo/LDAPUserManager.m b/SoObjects/SOGo/LDAPUserManager.m index 6dcdd3af..0e116885 100644 --- a/SoObjects/SOGo/LDAPUserManager.m +++ b/SoObjects/SOGo/LDAPUserManager.m @@ -303,11 +303,17 @@ static NSString *defaultMailDomain = nil; LDAPSource *currentSource; NSString *cn, *c_uid; NSArray *c_emails; - + BOOL access; + emails = [NSMutableArray array]; cn = nil; c_uid = nil; + [currentUser setObject: [NSNumber numberWithBool: YES] + forKey: @"CalendarAccess"]; + [currentUser setObject: [NSNumber numberWithBool: YES] + forKey: @"MailAccess"]; + ldapSources = [sources objectEnumerator]; currentSource = [ldapSources nextObject]; while (currentSource) @@ -322,6 +328,14 @@ static NSString *defaultMailDomain = nil; c_emails = [userEntry objectForKey: @"c_emails"]; if ([c_emails count]) [emails addObjectsFromArray: c_emails]; + access = [[userEntry objectForKey: @"CalendarAccess"] boolValue]; + if (!access) + [currentUser setObject: [NSNumber numberWithBool: NO] + forKey: @"CalendarAccess"]; + access = [[userEntry objectForKey: @"MailAccess"] boolValue]; + if (!access) + [currentUser setObject: [NSNumber numberWithBool: NO] + forKey: @"MailAccess"]; } currentSource = [ldapSources nextObject]; } diff --git a/SoObjects/SOGo/SOGoUser.h b/SoObjects/SOGo/SOGoUser.h index d8f80102..bc051398 100644 --- a/SoObjects/SOGo/SOGoUser.h +++ b/SoObjects/SOGo/SOGoUser.h @@ -124,6 +124,9 @@ extern NSString *SOGoWeekStartFirstFullWeek; - (BOOL) isSuperUser; +/* module access */ +- (BOOL) canAccessModule: (NSString *) module; + /* folders */ - (SOGoUserFolder *) homeFolderInContext: (id) context; diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index ee123359..bdf37fa0 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -622,4 +622,15 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek"; return [superUsernames containsObject: login]; } +/* module access */ +- (BOOL) canAccessModule: (NSString *) module +{ + NSString *accessValue; + + accessValue = [self _fetchFieldForUser: + [NSString stringWithFormat: @"%@Access", module]]; + + return [accessValue boolValue]; +} + @end /* SOGoUser */ diff --git a/SoObjects/SOGo/SOGoUserFolder.m b/SoObjects/SOGo/SOGoUserFolder.m index 5003c54d..035cbcb4 100644 --- a/SoObjects/SOGo/SOGoUserFolder.m +++ b/SoObjects/SOGo/SOGoUserFolder.m @@ -24,6 +24,8 @@ #import #import +#import + #import #import @@ -64,12 +66,18 @@ - (NSArray *) toManyRelationshipKeys { - static NSArray *children = nil; + NSMutableArray *children; + SOGoUser *currentUser; + + children = [NSMutableArray arrayWithCapacity: 4]; - if (!children) - children = [[NSArray alloc] initWithObjects: - @"Calendar", @"Contacts", @"Mail", - @"Preferences", nil]; + currentUser = [context activeUser]; + if ([currentUser canAccessModule: @"Calendar"]) + [children addObject: @"Calendar"]; + [children addObject: @"Contacts"]; + if ([currentUser canAccessModule: @"Mail"]) + [children addObject: @"Mail"]; + [children addObject: @"Preferences"]; return children; } @@ -149,12 +157,15 @@ acquire: (BOOL) _flag { id obj; + SOGoUser *currentUser; /* first check attributes directly bound to the application */ obj = [super lookupName: _key inContext: _ctx acquire: NO]; if (!obj) { - if ([_key isEqualToString: @"Calendar"]) + currentUser = [_ctx activeUser]; + if ([_key isEqualToString: @"Calendar"] + && [currentUser canAccessModule: _key]) obj = [self privateCalendars: @"Calendar" inContext: _ctx]; // if (![_key isEqualToString: @"Calendar"]) // obj = [obj lookupName: [_key pathExtension] @@ -163,7 +174,8 @@ obj = [self privateContacts: _key inContext: _ctx]; // else if ([_key isEqualToString: @"Groups"]) // obj = [self groupsFolder: _key inContext: _ctx]; - else if ([_key isEqualToString: @"Mail"]) + else if ([_key isEqualToString: @"Mail"] + && [currentUser canAccessModule: _key]) obj = [self mailAccountsFolder: _key inContext: _ctx]; else if ([_key isEqualToString: @"Preferences"]) obj = [$(@"SOGoPreferencesFolder") objectWithName: _key diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index 47f9433c..b3565dae 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -345,6 +345,24 @@ && [user isSuperUser]); } +- (BOOL) userHasCalendarAccess +{ + SOGoUser *user; + + user = [context activeUser]; + + return [user canAccessModule: @"Calendar"]; +} + +- (BOOL) userHasMailAccess +{ + SOGoUser *user; + + user = [context activeUser]; + + return [user canAccessModule: @"Mail"]; +} + /* browser/os identification */ - (BOOL) isCompatibleBrowser diff --git a/UI/Common/WODirectAction+SOGo.h b/UI/Common/WODirectAction+SOGo.h index 067046b6..aa518c6d 100644 --- a/UI/Common/WODirectAction+SOGo.h +++ b/UI/Common/WODirectAction+SOGo.h @@ -34,6 +34,8 @@ - (WOResponse *) responseWith204; - (WOResponse *) redirectToLocation: (NSString *) newLocation; +- (NSString *) labelForKey: (NSString *) _str; + @end #endif /* WODIRECTACTION_SOGO_H */ diff --git a/UI/Common/WODirectAction+SOGo.m b/UI/Common/WODirectAction+SOGo.m index 13c8b86d..6c1c3adf 100644 --- a/UI/Common/WODirectAction+SOGo.m +++ b/UI/Common/WODirectAction+SOGo.m @@ -20,10 +20,14 @@ * Boston, MA 02111-1307, USA. */ +#import -#import +#import #import +#import +#import + #import "WODirectAction+SOGo.h" @implementation WODirectAction (SOGoExtension) @@ -53,5 +57,33 @@ return response; } +- (NSString *) labelForKey: (NSString *) key +{ + NSString *userLanguage, *label; + NSArray *paths; + NSBundle *bundle; + NSDictionary *strings; + + bundle = [NSBundle bundleForClass: [self class]]; + if (!bundle) + bundle = [NSBundle mainBundle]; + userLanguage = [[context activeUser] language]; + paths = [bundle pathsForResourcesOfType: @"strings" + inDirectory: [NSString stringWithFormat: @"%@.lproj", + userLanguage] + forLocalization: userLanguage]; + if ([paths count] > 0) + { + strings = [NSDictionary + dictionaryFromStringsFile: [paths objectAtIndex: 0]]; + label = [strings objectForKey: key]; + if (!label) + label = key; + } + else + label = key; + + return label; +} @end diff --git a/UI/MailPartViewers/UIxMailPartAlternativeViewer.m b/UI/MailPartViewers/UIxMailPartAlternativeViewer.m index f93a18f7..72ed9b3a 100644 --- a/UI/MailPartViewers/UIxMailPartAlternativeViewer.m +++ b/UI/MailPartViewers/UIxMailPartAlternativeViewer.m @@ -92,6 +92,8 @@ if ((count = [_types count]) == 0) return NSNotFound; + if ((i = [_types indexOfObject:@"text/calendar"]) != NSNotFound) + return i; if ((i = [_types indexOfObject:@"text/html"]) != NSNotFound) return i; if ((i = [_types indexOfObject:@"text/plain"]) != NSNotFound) diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.m b/UI/MailPartViewers/UIxMailPartICalViewer.m index b30a8817..3ee2c8f6 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.m +++ b/UI/MailPartViewers/UIxMailPartICalViewer.m @@ -76,24 +76,6 @@ [item release]; item = nil; } -/* raw content handling */ - -- (NSStringEncoding) fallbackStringEncoding -{ - /* - iCalendar invitations sent by Outlook 2002 have the annoying bug that the - mail states an UTF-8 content encoding but the actual iCalendar content is - encoding in Latin-1 (or Windows Western?). - - As a result the content decoding will fail (TODO: always?). In this case we - try to decode with Latin-1. - - Note: we could check for the Outlook x-mailer, but it was considered better - to try Latin-1 as a fallback in any case (be tolerant). - */ - return NSISOLatin1StringEncoding; -} - /* accessors */ - (iCalCalendar *) inCalendar diff --git a/UI/MailPartViewers/UIxMailPartViewer.m b/UI/MailPartViewers/UIxMailPartViewer.m index 3639789c..f3a44db7 100644 --- a/UI/MailPartViewers/UIxMailPartViewer.m +++ b/UI/MailPartViewers/UIxMailPartViewer.m @@ -159,11 +159,6 @@ return content; } -- (NSStringEncoding) fallbackStringEncoding -{ - return 0; -} - - (NSString *) flatContentAsString { /* Note: we even have the line count in the body-info! */ @@ -183,32 +178,32 @@ s = [NSString stringWithData: content usingEncodingNamed: charset]; if (![s length]) { - /* latin 1 is used as a 8bit fallback charset... but does this - encoding accept any byte from 0 to 255? */ + /* UTF-8 is used as a 8bit fallback charset... */ s = [[NSString alloc] initWithData: content - encoding: NSISOLatin1StringEncoding]; + encoding: NSUTF8StringEncoding]; [s autorelease]; } if (!s) { - /* - Note: this can happend with iCalendar invitations sent by Outlook 2002. - It will mark the content as UTF-8 but actually deliver it as - Latin-1 (or Windows encoding?). + /* + iCalendar invitations sent by Outlook 2002 have the annoying bug that the + mail states an UTF-8 content encoding but the actual iCalendar content is + encoding in Latin-1 (or Windows Western?). + + As a result the content decoding will fail (TODO: always?). In this case we + try to decode with Latin-1. + + Note: we could check for the Outlook x-mailer, but it was considered better + to try Latin-1 as a fallback in any case (be tolerant). */ - [self errorWithFormat:@"could not convert content to text, charset: '%@'", - charset]; - if ([self fallbackStringEncoding] > 0) - { - s = [[NSString alloc] initWithData:content - encoding: [self fallbackStringEncoding]]; - if (s) - [s autorelease]; - else - [self errorWithFormat: - @"an attempt to use fallback encoding failed to."]; - } + + s = [[NSString alloc] initWithData:content + encoding: NSISOLatin1StringEncoding]; + if (!s) + [self errorWithFormat: @"an attempt to use" + @" NSISOLatin1StringEncoding as callback failed"]; + [s autorelease]; } } else diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 3bfff173..d745a638 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -147,6 +147,9 @@ "Compose Mail To" = "Compose Mail To"; "Create Filter From Message..." = "Create Filter From Message..."; +/* Image Popup menu */ +"View Image" = "View Image"; + /* Mailbox popup menus */ "Open in New Mail Window" = "Open in New Mail Window"; "Copy Folder Location" = "Copy Folder Location"; diff --git a/UI/MailerUI/French.lproj/Localizable.strings b/UI/MailerUI/French.lproj/Localizable.strings index 8a3d11b3..46d1e916 100644 --- a/UI/MailerUI/French.lproj/Localizable.strings +++ b/UI/MailerUI/French.lproj/Localizable.strings @@ -148,6 +148,9 @@ "Compose Mail To" = "Écrire à"; "Create Filter From Message..." = "Créer un filtre à partir du message..."; +/* Image Popup menu */ +"View Image" = "Voir l'image"; + /* Mailbox popup menus */ "Open in New Mail Window" = "Ouvrir dans une nouvelle fenétre"; "Copy Folder Location" = "Copier l'adresse du dossier"; diff --git a/UI/MailerUI/German.lproj/Localizable.strings b/UI/MailerUI/German.lproj/Localizable.strings index 6812036f..21748193 100644 --- a/UI/MailerUI/German.lproj/Localizable.strings +++ b/UI/MailerUI/German.lproj/Localizable.strings @@ -131,6 +131,9 @@ "Compose Mail To" = "Verfassen"; "Create Filter From Message..." = "Filter aus Nachricht erstellen..."; +/* Image Popup menu */ +"View Image" = "View Image"; + /* Mailbox popup menus */ "Open in New Mail Window" = "In neuem Fenster öffnen"; "Copy Folder Location" = "Ordner-Adresse kopieren"; diff --git a/UI/MailerUI/UIxMailAccountActions.m b/UI/MailerUI/UIxMailAccountActions.m index 69fe254d..ea243bca 100644 --- a/UI/MailerUI/UIxMailAccountActions.m +++ b/UI/MailerUI/UIxMailAccountActions.m @@ -168,7 +168,7 @@ if ([signature length]) { [newDraftMessage - setText: [NSString stringWithFormat: @"\r\n--\r\n%@", signature]]; + setText: [NSString stringWithFormat: @"\n-- \n%@", signature]]; save = YES; } if (save) diff --git a/UI/MailerUI/UIxMailEditor.m b/UI/MailerUI/UIxMailEditor.m index 2950bd39..31e33f02 100644 --- a/UI/MailerUI/UIxMailEditor.m +++ b/UI/MailerUI/UIxMailEditor.m @@ -443,7 +443,7 @@ static NSArray *infoKeys = nil; { result = [[self clientObject] sendMail]; if (!result) - result = [self jsCloseWithRefreshMethod: @"refreshFolderByType(\"sent\")"]; + result = [self jsCloseWithRefreshMethod: @"refreshCurrentFolder()"]; } else result = [self failedToSaveFormResponse]; diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 192d8267..c5f5bac4 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -483,7 +483,7 @@ static BOOL shouldDisplayPasswordChange = NO; - (NSString *) itemIdentityText { - return [item keysWithFormat: @"%{fullName} <%{email}>"]; + return [(NSDictionary *) item keysWithFormat: @"%{fullName} <%{email}>"]; } - (NSMutableDictionary *) defaultIdentity @@ -538,6 +538,16 @@ static BOOL shouldDisplayPasswordChange = NO; return [[request method] isEqualToString: @"POST"]; } +- (BOOL) userHasCalendarAccess +{ + return [user canAccessModule: @"Calendar"]; +} + +- (BOOL) userHasMailAccess +{ + return [user canAccessModule: @"Mail"]; +} + - (BOOL) shouldDisplayPasswordChange { return shouldDisplayPasswordChange; diff --git a/UI/SOGoUI/UIxComponent.m b/UI/SOGoUI/UIxComponent.m index a376ba60..50e6bf25 100644 --- a/UI/SOGoUI/UIxComponent.m +++ b/UI/SOGoUI/UIxComponent.m @@ -489,12 +489,6 @@ static BOOL uixDebugEnabled = NO; if ([lTable hasPrefix:@"$"]) lTable = [self valueForKeyPath:[lTable substringFromIndex:1]]; -#if 0 - if ([lVal hasPrefix:@"$"]) - lVal = [self valueForKeyPath:[lVal substringFromIndex:1]]; - -#endif - /* lookup string */ return [rm stringForKey: lKey inTableNamed: lTable diff --git a/UI/Scheduler/English.lproj/Localizable.strings b/UI/Scheduler/English.lproj/Localizable.strings index dbc48efc..aa8e4c08 100644 --- a/UI/Scheduler/English.lproj/Localizable.strings +++ b/UI/Scheduler/English.lproj/Localizable.strings @@ -413,5 +413,13 @@ validate_endbeforestart = "Enddate is before startdate!"; "Location" = "Location"; "(Private Event)" = "(Private Event)"; +vevent_class0 = "(Public event)"; +vevent_class1 = "(Private event)"; +vevent_class2 = "(Confidential event)"; + +vtodo_class0 = "(Public task)"; +vtodo_class1 = "(Private task)"; +vtodo_class2 = "(Confidential task)"; + "closeThisWindowMessage" = "Thank you! You may now close this window or view your "; "Multicolumn Day View" = "Multicolumn Day View"; diff --git a/UI/Scheduler/French.lproj/Localizable.strings b/UI/Scheduler/French.lproj/Localizable.strings index f04263bb..d0f002d0 100644 --- a/UI/Scheduler/French.lproj/Localizable.strings +++ b/UI/Scheduler/French.lproj/Localizable.strings @@ -412,5 +412,13 @@ validate_endbeforestart = "La date de fin est avant la date de début !"; "Location" = "Lieu"; "(Private Event)" = "(Événement privé)"; +vevent_class0 = "(Événement public)"; +vevent_class1 = "(Événement privé)"; +vevent_class2 = "(Événement confidentiel)"; + +vtodo_class0 = "(Tâche publique)"; +vtodo_class1 = "(Tâche privée)"; +vtodo_class2 = "(Tâche confidentielle)"; + "closeThisWindowMessage" = "Merci! Vous pouvez maintenant fermer cette fenêtre ou consulter votre "; "Multicolumn Day View" = "Multicolonne"; diff --git a/UI/Scheduler/German.lproj/Localizable.strings b/UI/Scheduler/German.lproj/Localizable.strings index da5f595b..1f65637c 100644 --- a/UI/Scheduler/German.lproj/Localizable.strings +++ b/UI/Scheduler/German.lproj/Localizable.strings @@ -400,5 +400,14 @@ validate_endbeforestart = "Ihr Beginn ist nach dem Ende"; "Location" = "Ort"; "(Private Event)" = "(Privater Termin)"; +/* event list */ +vevent_class0 = "(Public event)"; +vevent_class1 = "(Private event)"; +vevent_class2 = "(Confidential event)"; + +vtodo_class0 = "(Public task)"; +vtodo_class1 = "(Private task)"; +vtodo_class2 = "(Confidential task)"; + "closeThisWindowMessage" = "Vielen Dank! Sie können dieses Fenster jetzt schließen."; "Multicolumn Day View" = "Multicolonne"; diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 23d89990..a5147130 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -71,7 +71,7 @@ { if (!event) { - event = (iCalEvent *) [[self clientObject] component: NO secure: NO]; + event = (iCalEvent *) [[self clientObject] component: NO secure: YES]; [event retain]; } @@ -270,7 +270,7 @@ unsigned int minutes; iCalRecurrenceRule *rule; - event = (iCalEvent *) [[self clientObject] component: NO secure: YES]; + [self event]; if (event) { startDate = [event startDate]; @@ -382,7 +382,7 @@ iCalRecurrenceRule *rule; clientObject = [self clientObject]; - event = (iCalEvent *) [clientObject component: YES secure: NO]; + [self event]; [super takeValuesFromRequest: _rq inContext: _ctx]; diff --git a/UI/Scheduler/UIxCalListingActions.h b/UI/Scheduler/UIxCalListingActions.h index cb81e40f..99f4ee0e 100644 --- a/UI/Scheduler/UIxCalListingActions.h +++ b/UI/Scheduler/UIxCalListingActions.h @@ -23,8 +23,7 @@ #ifndef UIXCALLISTINGACTIONVIEW_H #define UIXCALLISTINGACTIONVIEW_H - -#import +#import @class NSCalendarDate; @class NSMutableDictionary; diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index 3346c739..f5a25d5a 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -185,22 +185,6 @@ } } -- (void) _updatePrivacyInComponent: (NSMutableDictionary *) component - fromFolder: (SOGoAppointmentFolder *) folder -{ - int privacyFlag; - NSString *roleString; - - privacyFlag = [[component objectForKey: @"classification"] intValue]; - roleString = [folder roleForComponentsWithAccessClass: privacyFlag - forUser: userLogin]; - if ([roleString isEqualToString: @"ComponentDAndTViewer"]) - { - [component setObject: @"" forKey: @"c_title"]; - [component setObject: @"" forKey: @"c_location"]; - } -} - - (SOGoAppointmentFolder *) _aptFolder: (NSString *) folder withClientObject: (SOGoAppointmentFolder *) clientObject { @@ -220,6 +204,17 @@ return aptFolder; } +- (void) _fixComponentTitle: (NSMutableDictionary *) component + withType: (NSString *) type +{ + NSString *labelKey; + + labelKey = [NSString stringWithFormat: @"%@_class%@", + type, [component objectForKey: @"c_classification"]]; + [component setObject: [self labelForKey: labelKey] + forKey: @"c_title"]; +} + - (NSArray *) _fetchFields: (NSArray *) fields forComponentOfType: (NSString *) component { @@ -232,28 +227,29 @@ marker = [NSNull null]; - clientObject = [self clientObject]; + clientObject = [self clientObject]; folders = [[clientObject subFolders] objectEnumerator]; currentFolder = [folders nextObject]; infos = [NSMutableArray array]; + while (currentFolder) { if ([currentFolder isActive]) { - currentInfos = [[currentFolder fetchCoreInfosFrom: startDate - to: endDate - title: title - component: component] objectEnumerator]; + currentInfos + = [[currentFolder fetchCoreInfosFrom: startDate + to: endDate + title: title + component: component] objectEnumerator]; while ((newInfo = [currentInfos nextObject])) { - [self _updatePrivacyInComponent: newInfo - fromFolder: currentFolder]; [newInfo setObject: [currentFolder nameInContainer] forKey: @"c_folder"]; - + if (![[newInfo objectForKey: @"c_title"] length]) + [self _fixComponentTitle: newInfo withType: component]; [infos addObject: [newInfo objectsForKeys: fields notFoundMarker: marker]]; } @@ -323,7 +319,7 @@ newEvents = [NSMutableArray array]; fields = [NSArray arrayWithObjects: @"c_name", @"c_folder", @"c_status", @"c_title", @"c_startdate", @"c_enddate", @"c_location", - @"c_isallday", nil]; + @"c_isallday", @"c_classification", nil]; events = [[self _fetchFields: fields forComponentOfType: @"vevent"] objectEnumerator]; oldEvent = [events nextObject]; @@ -399,7 +395,7 @@ [self _setupContext]; fields = [NSArray arrayWithObjects: @"c_name", @"c_folder", @"c_status", - @"c_title", @"c_enddate", nil]; + @"c_title", @"c_enddate", @"c_classification", nil]; tasks = [[self _fetchFields: fields forComponentOfType: @"vtodo"] objectEnumerator]; diff --git a/UI/Scheduler/UIxTaskEditor.m b/UI/Scheduler/UIxTaskEditor.m index 8ea3b9ed..759726dd 100644 --- a/UI/Scheduler/UIxTaskEditor.m +++ b/UI/Scheduler/UIxTaskEditor.m @@ -73,7 +73,7 @@ { if (!todo) { - todo = (iCalToDo *) [[self clientObject] component: NO secure: NO]; + todo = (iCalToDo *) [[self clientObject] component: NO secure: YES]; [todo retain]; } @@ -289,7 +289,7 @@ NSString *duration; unsigned int minutes; - todo = (iCalToDo *) [[self clientObject] component: NO secure: YES]; + [self todo]; if (todo) { startDate = [todo startDate]; @@ -383,10 +383,7 @@ - (void) takeValuesFromRequest: (WORequest *) _rq inContext: (WOContext *) _ctx { - SOGoTaskObject *clientObject; - - clientObject = [self clientObject]; - todo = (iCalToDo *) [clientObject component: YES secure: NO]; + [self todo]; [super takeValuesFromRequest: _rq inContext: _ctx]; @@ -438,12 +435,9 @@ - (id) changeStatusAction { - SOGoTaskObject *clientObject; NSString *newStatus; - clientObject = [self clientObject]; - todo = (iCalToDo *) [clientObject component: NO secure: NO]; - [todo retain]; + [self todo]; if (todo) { newStatus = [self queryParameterForKey: @"status"]; @@ -455,7 +449,7 @@ [todo setPercentComplete: @"0"]; [todo setStatus: @"IN-PROCESS"]; } - [clientObject saveComponent: todo]; + [[self clientObject] saveComponent: todo]; } return [self responseWith204]; diff --git a/UI/Templates/MailerUI/UIxMailMainFrame.wox b/UI/Templates/MailerUI/UIxMailMainFrame.wox index 669cb7fc..2e555aed 100644 --- a/UI/Templates/MailerUI/UIxMailMainFrame.wox +++ b/UI/Templates/MailerUI/UIxMailMainFrame.wox @@ -101,6 +101,21 @@ + + + +
diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index bb01400d..fde540e7 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -15,12 +15,16 @@
  • -
  • -
  • -
  • + +
  • +
    + +
  • +
  • +
  • @@ -45,43 +49,46 @@ />
-
-
- -
-
-
-
-
- -
-
-
-
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
-
-
- -
-