diff --git a/ActiveSync/SOGoActiveSyncDispatcher+Sync.m b/ActiveSync/SOGoActiveSyncDispatcher+Sync.m index f93098be8..88a6c8ec3 100644 --- a/ActiveSync/SOGoActiveSyncDispatcher+Sync.m +++ b/ActiveSync/SOGoActiveSyncDispatcher+Sync.m @@ -1338,7 +1338,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. changeDetected: (BOOL *) changeDetected maxSyncResponseSize: (int) theMaxSyncResponseSize { - NSString *collectionId, *realCollectionId, *syncKey, *davCollectionTag, *bodyPreferenceType, *mimeSupport, *lastServerKey, *syncKeyInCache, *folderKey; + NSString *collectionId, *realCollectionId, *syncKey, *davCollectionTag, *bodyPreferenceType, *mimeSupport, *mimeTruncation, *lastServerKey, *syncKeyInCache, *folderKey; NSMutableDictionary *folderMetadata, *folderOptions; NSMutableArray *supportedElements, *supportedElementNames; NSMutableString *changeBuffer, *commandsBuffer; @@ -1450,48 +1450,60 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. bodyPreferenceType = [[(id)[[(id)[theDocumentElement getElementsByTagName: @"BodyPreference"] lastObject] getElementsByTagName: @"Type"] lastObject] textValue]; if (!bodyPreferenceType) - { - bodyPreferenceType = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"BodyPreferenceType"]; + { + bodyPreferenceType = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"BodyPreferenceType"]; - // By default, send MIME mails. See #3146 for details. - if (!bodyPreferenceType) - bodyPreferenceType = @"4"; + // By default, send MIME mails. See #3146 for details. + if (!bodyPreferenceType) + bodyPreferenceType = @"4"; - mimeSupport = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMESupport"]; + mimeSupport = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMESupport"]; + mimeTruncation = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMETruncation"]; - if (!mimeSupport) - mimeSupport = @"1"; - } + if (!mimeSupport) + mimeSupport = @"1"; + + if (!mimeTruncation) + mimeTruncation = @"8"; + } else - { - mimeSupport = [[(id)[theDocumentElement getElementsByTagName: @"MIMESupport"] lastObject] textValue]; + { + mimeSupport = [[(id)[theDocumentElement getElementsByTagName: @"MIMESupport"] lastObject] textValue]; + mimeTruncation = [[(id)[theDocumentElement getElementsByTagName: @"MIMETruncation"] lastObject] textValue]; - if (!mimeSupport) + if (!mimeSupport) mimeSupport = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMESupport"]; - if (!mimeSupport) + if (!mimeSupport) mimeSupport = @"0"; - if ([mimeSupport isEqualToString: @"1"] && [bodyPreferenceType isEqualToString: @"4"]) + if (!mimeTruncation) + mimeTruncation = [[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMETruncation"]; + + if (!mimeTruncation) + mimeTruncation = @"8"; + + if ([mimeSupport isEqualToString: @"1"] && [bodyPreferenceType isEqualToString: @"4"]) bodyPreferenceType = @"2"; - else if ([mimeSupport isEqualToString: @"2"] && [bodyPreferenceType isEqualToString: @"4"]) + else if ([mimeSupport isEqualToString: @"2"] && [bodyPreferenceType isEqualToString: @"4"]) bodyPreferenceType = @"4"; - else if ([mimeSupport isEqualToString: @"0"] && [bodyPreferenceType isEqualToString: @"4"]) + else if ([mimeSupport isEqualToString: @"0"] && [bodyPreferenceType isEqualToString: @"4"]) bodyPreferenceType = @"2"; - - // Avoid writing to cache if there is nothing to change. - if (![[[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"BodyPreferenceType"] isEqualToString: bodyPreferenceType] || - ![[[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMESupport"] isEqualToString: mimeSupport]) - { - folderOptions = [[NSDictionary alloc] initWithObjectsAndKeys: mimeSupport, @"MIMESupport", bodyPreferenceType, @"BodyPreferenceType", nil]; - [folderMetadata setObject: folderOptions forKey: @"FolderOptions"]; - [self _setFolderMetadata: folderMetadata forKey: folderKey]; - } - } + // Avoid writing to cache if there is nothing to change. + if (![[[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"BodyPreferenceType"] isEqualToString: bodyPreferenceType] || + ![[[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMESupport"] isEqualToString: mimeSupport] || + ![[[folderMetadata objectForKey: @"FolderOptions"] objectForKey: @"MIMETruncation"] isEqualToString: mimeTruncation]) + { + folderOptions = [[NSDictionary alloc] initWithObjectsAndKeys: mimeSupport, @"MIMESupport", mimeTruncation, @"MIMETruncation", bodyPreferenceType, @"BodyPreferenceType", nil]; + [folderMetadata setObject: folderOptions forKey: @"FolderOptions"]; + [self _setFolderMetadata: folderMetadata forKey: folderKey]; + } + } [context setObject: bodyPreferenceType forKey: @"BodyPreferenceType"]; [context setObject: mimeSupport forKey: @"MIMESupport"]; + [context setObject: mimeTruncation forKey: @"MIMETruncation"]; [context setObject: [folderMetadata objectForKey: @"SupportedElements"] forKey: @"SupportedElements"]; // diff --git a/ActiveSync/SOGoMailObject+ActiveSync.m b/ActiveSync/SOGoMailObject+ActiveSync.m index 3228f6487..02ddbcccf 100644 --- a/ActiveSync/SOGoMailObject+ActiveSync.m +++ b/ActiveSync/SOGoMailObject+ActiveSync.m @@ -668,13 +668,14 @@ struct GlobalObjectId { NSMutableString *s; id value; - int preferredBodyType, mimeSupport, nativeBodyType; + int preferredBodyType, mimeSupport, mimeTruncation, nativeBodyType; uint32_t v; subtype = [[[self bodyStructure] valueForKey: @"subtype"] lowercaseString]; preferredBodyType = [[context objectForKey: @"BodyPreferenceType"] intValue]; mimeSupport = [[context objectForKey: @"MIMESupport"] intValue]; + mimeTruncation = [[context objectForKey: @"MIMETruncation"] intValue]; s = [NSMutableString string]; @@ -980,9 +981,71 @@ struct GlobalObjectId { AUTORELEASE(content); content = [content activeSyncRepresentationInContext: context]; + len = [content length]; truncated = 0; - len = [content length]; + // We handle MIMETruncation + switch (mimeTruncation) + { + case 0: + { + content = @""; + len = 0; + } + break; + case 1: + if ([content length] > 4096) + { + content = [content substringToIndex: 4096]; + len = 4096; truncated = 1; + } + break; + case 2: + if ([content length] > 5120) + { + content = [content substringToIndex: 5120]; + len = 5120; truncated = 1; + } + break; + case 3: + if ([content length] > 7168) + { + content = [content substringToIndex: 7168]; + len = 7168; truncated = 1; + } + break; + case 4: + if ([content length] > 10240) + { + content = [content substringToIndex: 10240]; + len = 10240; truncated = 1; + } + break; + case 5: + if ([content length] > 20480) + { + content = [content substringToIndex: 20480]; + len = 20480; truncated = 1; + } + break; + case 6: + if ([content length] > 51200) + { + content = [content substringToIndex: 51200]; + len = 51200; truncated = 1; + } + break; + case 7: + if ([content length] > 102400) + { + content = [content substringToIndex: 102400]; + len = 102400; truncated = 1; + } + break; + case 8: + default: + truncated = 0; + } if ([[[context request] headerForKey: @"MS-ASProtocolVersion"] isEqualToString: @"2.5"]) { @@ -990,7 +1053,7 @@ struct GlobalObjectId { [s appendFormat: @"%d", truncated]; } else - { + { [s appendString: @""]; // Set the correct type if client requested text/html but we got text/plain. @@ -1004,16 +1067,12 @@ struct GlobalObjectId { [s appendFormat: @"%d", truncated]; [s appendFormat: @""]; - - if (!truncated) - { - [s appendFormat: @"%@", content]; - [s appendFormat: @"%d", len]; - } + [s appendFormat: @"%@", content]; + [s appendFormat: @"%d", len]; [s appendString: @""]; } } - + // Attachments -namespace 16 attachmentKeys = [self fetchFileAttachmentKeys]; diff --git a/NEWS b/NEWS index 01d8b630b..80aa5b1c1 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,7 @@ Enhancements - [web] improved confirm dialogs for deletions - [web] allow resources to prevent invitations (#3410) - [web] warn when double-booking attendees and offer force save option + - [eas] now support EAS MIME truncation Bug fixes - [web] handle birthday dates before 1970 @@ -26,6 +27,7 @@ Bug fixes - [web] fixed batched delete of components (#3516) - [web] fixed mail draft autosave in preferences (#3519) - [web] fixed password change (#3496) + - [web] fixed saving of notification email for calendar changes (#3522) - [eas] allow EAS attachments get on 2nd-level mailboxes (#3505) - [eas] fix EAS bday shift (#3518) diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 0b45df89b..d79239658 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -652,7 +652,7 @@ else { iCalCalendar *calendar; - NSDictionary *values; + NSDictionary *values, *info; NSString *reason; iCalEvent *event; @@ -669,8 +669,10 @@ reason = [values keysWithFormat: [self labelForKey: @"Maximum number of simultaneous bookings (%{NumberOfSimultaneousBookings}) reached for resource \"%{Cn} %{SystemEmail}\". The conflicting event is \"%{EventTitle}\", and starts on %{StartDate}."]]; + info = [NSDictionary dictionaryWithObject: reason forKey: @"reject"]; + return [NSException exceptionWithHTTPStatus: 403 - reason: reason]; + reason: [info jsonRepresentation]]; } } // diff --git a/SoObjects/Appointments/SOGoComponentOccurence.m b/SoObjects/Appointments/SOGoComponentOccurence.m index ccef02f7c..5017502a6 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.m +++ b/SoObjects/Appointments/SOGoComponentOccurence.m @@ -1,6 +1,6 @@ /* SOGoComponentOccurence.m - this file is part of SOGo * - * Copyright (C) 2008-2014 Inverse inc. + * Copyright (C) 2008-2016 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -204,6 +204,13 @@ return [container saveComponent: newObject]; } +- (NSException *) saveComponent: (iCalRepeatableEntityObject *) newObject + force: (BOOL) forceSave +{ + return [container saveComponent: newObject + force: forceSave]; +} + #warning most of SOGoCalendarComponent and SOGoComponentOccurence share the same external interface... \ they should be siblings or SOGoComponentOccurence the parent class of SOGoCalendarComponent... - (NSException *) changeParticipationStatus: (NSString *) newStatus diff --git a/UI/Scheduler/UIxAppointmentActions.m b/UI/Scheduler/UIxAppointmentActions.m index 231edab81..b6791fac6 100644 --- a/UI/Scheduler/UIxAppointmentActions.m +++ b/UI/Scheduler/UIxAppointmentActions.m @@ -64,6 +64,7 @@ SOGoAppointmentFolder *targetCalendar, *sourceCalendar; SOGoAppointmentFolders *folders; BOOL forceSave; + id error; rq = [context request]; params = [[rq contentAsString] objectFromJSONString]; @@ -72,7 +73,7 @@ startDelta = [params objectForKey: @"start"]; durationDelta = [params objectForKey: @"duration"]; destionationCalendar = [params objectForKey: @"destination"]; - forceSave = NO; + forceSave = [[params objectForKey: @"ignoreConflicts"] boolValue]; if (daysDelta || startDelta || durationDelta) { @@ -146,8 +147,11 @@ if ([ex respondsToSelector: @selector(httpStatus)]) httpStatus = [ex httpStatus]; + error = [[ex reason] objectFromJSONString]; + if (error == nil) + error = [ex reason]; jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: - [ex reason], @"message", + error, @"message", nil]; response = [self responseWithStatus: httpStatus diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 438dc5f23..7a25ceec1 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -454,6 +454,7 @@ SOGoAppointmentObject *co; SoSecurityManager *sm; WORequest *request; + id error; unsigned int httpStatus; BOOL forceSave; @@ -477,7 +478,7 @@ else { [self setAttributes: params]; - forceSave = NO; + forceSave = [[params objectForKey: @"ignoreConflicts"] boolValue]; if ([event hasRecurrenceRules]) [self _adjustRecurrentRules]; @@ -533,9 +534,11 @@ if ([ex respondsToSelector: @selector(httpStatus)]) httpStatus = [ex httpStatus]; + error = [[ex reason] objectFromJSONString]; + if (error == nil) + error = [ex reason]; jsonResponse = [NSDictionary dictionaryWithObjectsAndKeys: - [ex reason], @"message", - nil]; + error, @"message", nil]; } else { diff --git a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox index 134b25b54..6a1427bc4 100644 --- a/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox +++ b/UI/Templates/SchedulerUI/UIxAppointmentEditorTemplate.wox @@ -275,7 +275,7 @@ - + @@ -287,6 +287,65 @@ + + + +
+
+ + close + +
+
+ person {{editor.attendeeConflictError.attendee_name}} ({{editor.attendeeConflictError.attendee_email}}) +
+
+ schedule +
+ +
{{conflict.startDate}} trending_flat
+
+
+ +
{{conflict.endDate}}
+
+
+
+ + + + + + + + + + + + + + +
{{editor.attendeeConflictError.reject}}
+ + close + +
+ + + + + + + + diff --git a/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox b/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox index 5b0bd694b..6f2bb09a6 100644 --- a/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox +++ b/UI/Templates/SchedulerUI/UIxAppointmentViewTemplate.wox @@ -211,26 +211,39 @@ - - - - - - - - - + + repeat_one + + + + + repeat + +
- - - - - - + + + arrow_drop_down + + + + + repeat_one + + + + + repeat + + + +
@@ -251,49 +264,41 @@ label:aria-label="Delete Event" ng-click="$mdOpenMenu()" md-menu-origin="md-menu-origin"> - + arrow_drop_down - - - - - - - - - + + repeat_one + + + + + repeat + +
- - - - - - - - - + - + arrow_drop_down - - + + repeat_one - - - - - - + + + + repeat + +
diff --git a/UI/Templates/SchedulerUI/UIxCalMainView.wox b/UI/Templates/SchedulerUI/UIxCalMainView.wox index d448c3d4e..3d11e4aca 100644 --- a/UI/Templates/SchedulerUI/UIxCalMainView.wox +++ b/UI/Templates/SchedulerUI/UIxCalMainView.wox @@ -737,4 +737,46 @@ + + + diff --git a/UI/Templates/SchedulerUI/UIxCalendarProperties.wox b/UI/Templates/SchedulerUI/UIxCalendarProperties.wox index d05041da0..174ff56f9 100644 --- a/UI/Templates/SchedulerUI/UIxCalendarProperties.wox +++ b/UI/Templates/SchedulerUI/UIxCalendarProperties.wox @@ -82,7 +82,7 @@ diff --git a/UI/WebServerResources/js/Common/navController.js b/UI/WebServerResources/js/Common/navController.js index a6f78b5d8..934de0961 100644 --- a/UI/WebServerResources/js/Common/navController.js +++ b/UI/WebServerResources/js/Common/navController.js @@ -66,7 +66,7 @@ function onHttpError(event, response) { var message; - if (response.data && response.data.message) + if (response.data && response.data.message && angular.isString(response.data.message)) message = response.data.message; else if (response.status) message = response.statusText; @@ -85,7 +85,7 @@ position: 'top right' }); else - console.debug('untrap error'); + $log.debug('untrap error'); } // Listen to HTTP errors broadcasted from HTTP interceptor diff --git a/UI/WebServerResources/js/Scheduler/CalendarListController.js b/UI/WebServerResources/js/Scheduler/CalendarListController.js index 93793a05c..644371def 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarListController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarListController.js @@ -162,8 +162,7 @@ component.setDelta(coordinates.duration * 15); newComponent(null, component).finally(function() { $timeout(function() { - Component.$ghost.pointerHandler = null; - Component.$ghost.component = null; + Component.$resetGhost(); }); }); } @@ -186,8 +185,11 @@ // Immediately perform the adjustments component.$adjust(params).then(function() { $rootScope.$emit('calendars:list'); + }, function(response) { + onComponentAdjustError(response, component, params); + }).finally(function() { $timeout(function() { - Component.$ghost = {}; + Component.$resetGhost(); }); }); else if (component.occurrenceId) { @@ -199,7 +201,7 @@ params: params }, template: [ - '', + '', ' ', '

' + l('editRepeatingItem') + '

', '
', @@ -214,7 +216,7 @@ $rootScope.$emit('calendars:list'); }).finally(function() { $timeout(function() { - Component.$ghost = {}; + Component.$resetGhost(); }); }); } @@ -226,13 +228,58 @@ RecurrentComponentDialogController.$inject = ['$scope', '$mdDialog', 'component', 'params']; function RecurrentComponentDialogController($scope, $mdDialog, component, params) { $scope.updateThisOccurrence = function() { - component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + component.$adjust(params).then($mdDialog.hide, function(response) { + $mdDialog.cancel().then(function() { + onComponentAdjustError(response, component, params); + }); + }); }; $scope.updateAllOccurrences = function() { delete component.occurrenceId; - component.$adjust(params).then($mdDialog.hide, $mdDialog.cancel); + component.$adjust(params).then($mdDialog.hide, function(response) { + $mdDialog.cancel().then(function() { + onComponentAdjustError(response, component, params); + }); + }); }; } + + function onComponentAdjustError(response, component, params) { + if (response.status == 403 && + response.data && response.data.message && angular.isObject(response.data.message)) { + $mdDialog.show({ + parent: angular.element(document.body), + clickOutsideToClose: false, + escapeToClose: false, + templateUrl: 'UIxAttendeeConflictDialog', + controller: AttendeeConflictDialogController, + controllerAs: '$AttendeeConflictDialogController', + locals: { + component: component, + params: params, + conflictError: response.data.message + } + }).then(function() { + $rootScope.$emit('calendars:list'); + }); + } + } + + /** + * @ngInject + */ + AttendeeConflictDialogController.$inject = ['$scope', '$mdDialog', 'component', 'params', 'conflictError']; + function AttendeeConflictDialogController($scope, $mdDialog, component, params, conflictError) { + var vm = this; + + vm.conflictError = conflictError; + vm.cancel = $mdDialog.cancel; + vm.save = save; + + function save() { + component.$adjust(angular.extend({ ignoreConflicts: true }, params)).then($mdDialog.hide); + } + } } function filter(filterpopup) { diff --git a/UI/WebServerResources/js/Scheduler/Component.service.js b/UI/WebServerResources/js/Scheduler/Component.service.js index 7390fbe84..a6ab49487 100644 --- a/UI/WebServerResources/js/Scheduler/Component.service.js +++ b/UI/WebServerResources/js/Scheduler/Component.service.js @@ -414,6 +414,15 @@ }); }; + /** + * @function $resetGhost + * @desc Prepare the ghost object for the next drag by resetting appropriate attributes + */ + Component.$resetGhost = function() { + this.$ghost.pointerHandler = null; + this.$ghost.component = null; + }; + /** * @function $parseDate * @desc Parse a date string with format YYYY-MM-DDTHH:MM @@ -1035,8 +1044,9 @@ * @function $save * @memberof Component.prototype * @desc Save the component to the server. + * @param {object} extraAttributes - additional attributes to send to the server */ - Component.prototype.$save = function() { + Component.prototype.$save = function(extraAttributes) { var _this = this, options, path, component, date, dlp; component = this.$omit(); @@ -1106,6 +1116,8 @@ if (this.occurrenceId) path.push(this.occurrenceId); + angular.extend(component, extraAttributes); + return Component.$$resource.save(path.join('/'), component, options) .then(function(data) { // Make a copy of the data for an eventual reset diff --git a/UI/WebServerResources/js/Scheduler/ComponentController.js b/UI/WebServerResources/js/Scheduler/ComponentController.js index 3bc62fdfd..e5ab49775 100644 --- a/UI/WebServerResources/js/Scheduler/ComponentController.js +++ b/UI/WebServerResources/js/Scheduler/ComponentController.js @@ -162,6 +162,7 @@ vm.addAttachUrl = addAttachUrl; vm.cancel = cancel; vm.save = save; + vm.attendeeConflictError = false; vm.attendeesEditor = { days: getDays(), hours: getHours() @@ -217,15 +218,18 @@ } } - function save(form) { + function save(form, options) { if (form.$valid) { - vm.component.$save() + vm.component.$save(options) .then(function(data) { $rootScope.$emit('calendars:list'); $mdDialog.hide(); Alarm.getAlarms(); - }, function(data, status) { - $log.debug('failed'); + }, function(response) { + if (response.status == 403 && + response.data && response.data.message && + angular.isObject(response.data.message)) + vm.attendeeConflictError = response.data.message; }); } } diff --git a/UI/WebServerResources/scss/components/dialog/dialog.scss b/UI/WebServerResources/scss/components/dialog/dialog.scss index f03da576a..415393a35 100644 --- a/UI/WebServerResources/scss/components/dialog/dialog.scss +++ b/UI/WebServerResources/scss/components/dialog/dialog.scss @@ -19,6 +19,22 @@ body.popup { } } +md-dialog { + md-dialog-content { + &.sg-dialog-message { + // Prepare content to receive a "close" button on the right + align-items: flex-start; + display: flex; + flex-shrink: 0; + transition: background-color $swift-ease-in-duration $swift-ease-in-timing-function; + &.ng-hide { + background-color: white !important; + transition-duration: 0s; + } + } + } +} + @media (max-width: $layout-breakpoint-xs) { md-dialog { &[flex-xs="100"],