mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-08-29 02:07:36 +00:00
the whole server built html of an iMIP card is rendered through sg-compile
(Message.service.js sets part.compile for UIxMailPartICalViewer), and the user
comment and the description are emitted with escapeHTML="NO". event data from
the invitation is therefore both an angular template and raw html.
47133fdf3 closed the description against interpolation with ng-non-bindable.
this closes both paths for every remaining field:
* ng-non-bindable on the user comment, the location, the organizer common name
and the raw body shown when the calendar cannot be parsed
* the user comment and the description are read through accessors that escape
them, so the url detection and insertBR produce the only markup left in the
output
the marker sits on the leaf containers, not on md-card-content, so the buttons,
the delegation autocomplete and the attendee chips keep being compiled. url
detection still turns a bare url in a comment or a description into a link.
627 lines
15 KiB
Objective-C
627 lines
15 KiB
Objective-C
/*
|
|
Copyright (C) 2006-2021 Inverse inc.
|
|
|
|
This file is part of SOGo.
|
|
|
|
SOGo 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.
|
|
|
|
SOGo 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 SOGo; see the file COPYING. If not, write to the
|
|
Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
|
|
02111-1307, USA.
|
|
*/
|
|
|
|
/*
|
|
UIxMailPartICalViewer
|
|
|
|
Show plain/calendar mail parts.
|
|
*/
|
|
|
|
#import <Foundation/NSDictionary.h>
|
|
#import <Foundation/NSException.h>
|
|
#import <Foundation/NSValue.h>
|
|
|
|
|
|
#import <NGExtensions/NSCalendarDate+misc.h>
|
|
#import <NGExtensions/NSObject+Logs.h>
|
|
#import <NGExtensions/NSString+misc.h>
|
|
|
|
#import <NGImap4/NGImap4EnvelopeAddress.h>
|
|
|
|
#import <NGCards/iCalCalendar.h>
|
|
#import <NGCards/iCalEvent.h>
|
|
#import <NGCards/iCalPerson.h>
|
|
|
|
#import <SOGo/SOGoDateFormatter.h>
|
|
#import <SOGo/SOGoUser.h>
|
|
#import <SOGo/SOGoUserFolder.h>
|
|
#import <SOGo/SOGoUserDefaults.h>
|
|
#import <SOGo/SOGoUserManager.h>
|
|
#import <SOGo/NSString+Utilities.h>
|
|
#import <Appointments/iCalEntityObject+SOGo.h>
|
|
#import <Appointments/SOGoAppointmentFolder.h>
|
|
#import <Appointments/SOGoAppointmentFolders.h>
|
|
#import <Appointments/SOGoAppointmentObject.h>
|
|
#import <Mailer/SOGoMailObject.h>
|
|
#import <Mailer/SOGoMailBodyPart.h>
|
|
|
|
#import "UIxMailPartICalViewer.h"
|
|
|
|
@implementation UIxMailPartICalViewer
|
|
|
|
- (void) dealloc
|
|
{
|
|
[storedEventObject release];
|
|
[storedEvent release];
|
|
[inCalendar release];
|
|
[dateFormatter release];
|
|
[super dealloc];
|
|
}
|
|
|
|
/* maintain caches */
|
|
|
|
- (void) resetPathCaches
|
|
{
|
|
[super resetPathCaches];
|
|
inEvent = nil;
|
|
[inCalendar release]; inCalendar = nil;
|
|
[storedEventObject release]; storedEventObject = nil;
|
|
[storedEvent release]; storedEvent = nil;
|
|
}
|
|
|
|
/* accessors */
|
|
|
|
- (iCalCalendar *) inCalendar
|
|
{
|
|
if (!inCalendar)
|
|
{
|
|
inCalendar = [iCalCalendar parseSingleFromSource: [self flatContentAsString]];
|
|
[inCalendar retain];
|
|
}
|
|
|
|
return inCalendar;
|
|
}
|
|
|
|
- (BOOL) couldParseCalendar
|
|
{
|
|
return ([self inCalendar] != nil);
|
|
}
|
|
|
|
- (iCalEvent *) inEvent
|
|
{
|
|
NSArray *events;
|
|
|
|
if (!inEvent)
|
|
{
|
|
events = [[self inCalendar] events];
|
|
if ([events count] > 0)
|
|
inEvent = [events objectAtIndex: 0];
|
|
}
|
|
|
|
return inEvent;
|
|
}
|
|
|
|
/* formatters */
|
|
|
|
- (SOGoDateFormatter *) dateFormatter
|
|
{
|
|
if (!dateFormatter)
|
|
{
|
|
dateFormatter = [[context activeUser] dateFormatterInContext: context];
|
|
[dateFormatter retain];
|
|
}
|
|
|
|
return dateFormatter;
|
|
}
|
|
|
|
- (NSString *) _personForDisplay: (iCalPerson *) person
|
|
{
|
|
NSString *fn, *email, *result;
|
|
|
|
fn = [person cnWithoutQuotes];
|
|
email = [person rfc822Email];
|
|
if ([fn length])
|
|
result = [NSString stringWithFormat: @"%@ <%@>",
|
|
fn, email];
|
|
else
|
|
result = email;
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSCalendarDate *) startCalendarDate
|
|
{
|
|
NSCalendarDate *date;
|
|
SOGoUserDefaults *ud;
|
|
|
|
date = [[self inEvent] startDate];
|
|
ud = [[context activeUser] userDefaults];
|
|
if (![[self inEvent] isAllDay])
|
|
[date setTimeZone: [ud timeZone]];
|
|
|
|
return date;
|
|
}
|
|
|
|
/*
|
|
In v3.0, we moved the template's logic here to format
|
|
the event's start/end date-time. The previous logic was:
|
|
|
|
<dd><var:string value="startDate" />
|
|
<var:if condition="inEvent.isAllDay" const:negate="YES">
|
|
<var:string value="startTime" />
|
|
</var:if>
|
|
<var:if condition="isEndDateOnSameDay">
|
|
<var:if condition="inEvent.isAllDay" const:negate="YES">
|
|
<var:string label:value="to" />
|
|
<var:string value="endTime" />
|
|
</var:if>
|
|
</var:if>
|
|
<var:if condition="isEndDateOnSameDay" const:negate="YES">
|
|
<var:string label:value="to" />
|
|
<var:string value="endDate" />
|
|
<var:if condition="inEvent.isAllDay" const:negate="YES">
|
|
<var:string value="endTime" />
|
|
</var:if>
|
|
</var:if>
|
|
</dd>
|
|
|
|
*/
|
|
- (NSString *) formattedDateTime
|
|
{
|
|
NSMutableString *s;
|
|
|
|
s = [NSMutableString string];
|
|
|
|
[s appendString: [self startDate]];
|
|
|
|
if (![[self inEvent] isAllDay])
|
|
[s appendFormat: @" %@", [self startTime]];
|
|
|
|
if ([self isEndDateOnSameDay] &&
|
|
![[self inEvent] isAllDay])
|
|
{
|
|
[s appendFormat: @" %@", [self labelForKey: @"to_time"]];
|
|
[s appendFormat: @" %@", [self endTime]];
|
|
}
|
|
else if (![self isEndDateOnSameDay])
|
|
{
|
|
[s appendFormat: @" %@", [self labelForKey: @"to_date"]];
|
|
[s appendFormat: @" %@", [self endDate]];
|
|
|
|
if (![[self inEvent] isAllDay])
|
|
[s appendFormat: @" %@", [self endTime]];
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
- (NSString *) startDate
|
|
{
|
|
return [[self dateFormatter] formattedDate: [self startCalendarDate]];
|
|
}
|
|
|
|
- (NSString *) startTime
|
|
{
|
|
return [[self dateFormatter] formattedTime: [self startCalendarDate]];
|
|
}
|
|
|
|
- (NSCalendarDate *) endCalendarDate
|
|
{
|
|
NSCalendarDate *date;
|
|
SOGoUserDefaults *ud;
|
|
|
|
date = [[self inEvent] endDate];
|
|
ud = [[context activeUser] userDefaults];
|
|
if (![[self inEvent] isAllDay])
|
|
[date setTimeZone: [ud timeZone]];
|
|
|
|
return date;
|
|
}
|
|
|
|
- (NSString *) endDate
|
|
{
|
|
NSCalendarDate *aDate;
|
|
if ([[self inEvent] isAllDay])
|
|
aDate = [[self endCalendarDate] dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 seconds:-1];
|
|
else
|
|
aDate = [self endCalendarDate];
|
|
|
|
return [[self dateFormatter] formattedDate: aDate];
|
|
}
|
|
|
|
- (NSString *) endTime
|
|
{
|
|
return [[self dateFormatter] formattedTime: [self endCalendarDate]];
|
|
}
|
|
|
|
- (NSTimeInterval) duration
|
|
{
|
|
return [[self endCalendarDate] timeIntervalSinceDate:[self startCalendarDate]];
|
|
}
|
|
|
|
- (BOOL) isEndDateOnSameDay
|
|
{
|
|
if ([[self inEvent] isAllDay])
|
|
return [self duration] <= 86400;
|
|
|
|
return [[self startCalendarDate] isDateOnSameDay: [self endCalendarDate]];
|
|
}
|
|
|
|
/* calendar folder support */
|
|
- (BOOL) hasCalendarAccess
|
|
{
|
|
return [[context activeUser] canAccessModule: @"Calendar"];
|
|
}
|
|
|
|
/* lookup object in the user calendars */
|
|
- (SOGoAppointmentObject *) storedEventObject
|
|
{
|
|
NSArray *folders;
|
|
NSEnumerator *e;
|
|
NSString *uid, *cname, *userLogin;
|
|
SOGoAppointmentFolder *folder;
|
|
SOGoUser *user;
|
|
|
|
if (!storedEventFetched)
|
|
{
|
|
if ([self hasCalendarAccess])
|
|
{
|
|
user = [context activeUser];
|
|
userLogin = [user login];
|
|
uid = [[self inEvent] uid];
|
|
storedEventObject = nil;
|
|
|
|
folders = [[user calendarsFolderInContext: context] subFolders];
|
|
e = [folders objectEnumerator];
|
|
while (storedEventObject == nil && (folder = [e nextObject]))
|
|
{
|
|
if ([[folder ownerInContext: nil] isEqualToString: userLogin])
|
|
{
|
|
cname = [folder resourceNameForEventUID: uid];
|
|
if (cname)
|
|
{
|
|
storedEventObject = [folder lookupName: cname inContext: context
|
|
acquire: NO];
|
|
if ([storedEventObject isKindOfClass: [NSException class]])
|
|
storedEventObject = nil;
|
|
else
|
|
[storedEventObject retain];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
storedEventFetched = YES;
|
|
}
|
|
|
|
return storedEventObject;
|
|
}
|
|
|
|
- (BOOL) isEventStoredInCalendar
|
|
{
|
|
NSCalendarDate *recurrenceId;
|
|
NSString *recurrenceTime;
|
|
BOOL isInCalendar;
|
|
|
|
recurrenceId = [[self inEvent] recurrenceId];
|
|
|
|
if (recurrenceId)
|
|
{
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
|
|
isInCalendar = ([[self storedEventObject] lookupOccurrence: recurrenceTime] != nil);
|
|
}
|
|
else
|
|
isInCalendar = ([self storedEventObject] != nil);
|
|
|
|
return isInCalendar;
|
|
}
|
|
|
|
- (iCalEvent *) storedEvent
|
|
{
|
|
if (!storedEvent)
|
|
{
|
|
NSCalendarDate *recurrenceId;
|
|
|
|
recurrenceId = [[self inEvent] recurrenceId];
|
|
|
|
if (recurrenceId == nil)
|
|
storedEvent = [[self storedEventObject] component: NO secure: NO];
|
|
else
|
|
{
|
|
// Find the specific occurence within the repeating vEvent.
|
|
NSString *recurrenceTime;
|
|
iCalPerson *organizer;
|
|
|
|
recurrenceTime = [NSString stringWithFormat: @"%f", [recurrenceId timeIntervalSince1970]];
|
|
storedEvent = (iCalEvent*)[[self storedEventObject] lookupOccurrence: recurrenceTime];
|
|
|
|
if (storedEvent == nil)
|
|
// If no occurence found, create one
|
|
storedEvent = (iCalEvent*)[storedEventObject newOccurenceWithID: recurrenceTime];
|
|
|
|
// Add organizer to occurence if not present
|
|
organizer = [storedEvent organizer];
|
|
if (organizer == nil || [organizer isVoid])
|
|
{
|
|
organizer = [(iCalEntityObject *)[[storedEvent parent] firstChildWithTag: [storedEventObject componentTag]] organizer];
|
|
[storedEvent setOrganizer: organizer];
|
|
}
|
|
}
|
|
|
|
[storedEvent retain];
|
|
}
|
|
|
|
return storedEvent;
|
|
}
|
|
|
|
/* organizer tracking */
|
|
|
|
- (NSString *) loggedInUserEMail
|
|
{
|
|
NSDictionary *identity;
|
|
|
|
identity = [[context activeUser] primaryIdentity];
|
|
|
|
return [identity objectForKey: @"email"];
|
|
}
|
|
|
|
- (iCalEvent *) authorativeEvent
|
|
{
|
|
iCalEvent *authorativeEvent;
|
|
NSString *comment, *cleanComment;
|
|
|
|
[self storedEvent];
|
|
if (!storedEvent
|
|
|| ([storedEvent compare: [self inEvent]] == NSOrderedAscending))
|
|
authorativeEvent = inEvent;
|
|
else
|
|
authorativeEvent = [self storedEvent];
|
|
|
|
//Clean authorative event
|
|
if ((comment = [authorativeEvent comment])) {
|
|
cleanComment = [comment stringWithoutHTMLInjection: NO stripAngular:YES];
|
|
[authorativeEvent setComment: cleanComment];
|
|
}
|
|
|
|
|
|
|
|
return authorativeEvent;
|
|
}
|
|
|
|
- (BOOL) isLoggedInUserTheOrganizer
|
|
{
|
|
return [[self authorativeEvent] userIsOrganizer: [context activeUser]];
|
|
}
|
|
|
|
- (BOOL) isLoggedInUserAnAttendee
|
|
{
|
|
return [[self authorativeEvent] userIsAttendee: [context activeUser]];
|
|
}
|
|
|
|
/* derived fields */
|
|
- (BOOL) hasOrganizer
|
|
{
|
|
iCalPerson *organizer;
|
|
|
|
organizer = [[self authorativeEvent] organizer];
|
|
|
|
if ([organizer isVoid])
|
|
return NO;
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSString *) organizerDisplayName
|
|
{
|
|
iCalPerson *organizer;
|
|
NSString *value;
|
|
|
|
organizer = [[self authorativeEvent] organizer];
|
|
value = [self _personForDisplay: organizer];
|
|
|
|
return value;
|
|
}
|
|
|
|
/* replies */
|
|
|
|
- (NGImap4EnvelopeAddress *) replySenderAddress
|
|
{
|
|
/*
|
|
The iMIP reply is the sender of the mail, the 'attendees' are NOT set to
|
|
the actual attendees. BUT the attendee field contains the reply-status!
|
|
*/
|
|
id tmp;
|
|
|
|
tmp = [[self clientObject] fromEnvelopeAddresses];
|
|
if ([tmp count] == 0) return nil;
|
|
return [tmp objectAtIndex:0];
|
|
}
|
|
|
|
- (NSString *) replySenderEMail
|
|
{
|
|
return [[self replySenderAddress] email];
|
|
}
|
|
|
|
- (NSString *) replySenderBaseEMail
|
|
{
|
|
return [[self replySenderAddress] baseEMail];
|
|
}
|
|
|
|
- (iCalPerson *) inReplyAttendee
|
|
{
|
|
NSArray *attendees;
|
|
|
|
attendees = [[self inEvent] attendees];
|
|
if ([attendees count] == 0)
|
|
return nil;
|
|
if ([attendees count] > 1)
|
|
[self warnWithFormat:@"More than one attendee in REPLY: %@", attendees];
|
|
|
|
return [attendees objectAtIndex:0];
|
|
}
|
|
|
|
- (iCalPerson *) currentUserAttendee
|
|
{
|
|
iCalPerson *currentUser;
|
|
|
|
currentUser = [[self authorativeEvent] userAsAttendee: [context activeUser]];
|
|
|
|
return currentUser;
|
|
}
|
|
|
|
- (iCalPerson *) _attendeeFromMailSenderInEvent: (iCalEvent *) e
|
|
{
|
|
NSString *baseEmail;
|
|
iCalPerson *p;
|
|
|
|
baseEmail = [self replySenderBaseEMail];
|
|
|
|
/*
|
|
Note: -findAttendeeWithEmail: does not parse the email!
|
|
*/
|
|
p = [e findAttendeeWithEmail: baseEmail];
|
|
if (!p)
|
|
p = [e findAttendeeWithEmail:[self replySenderEMail]];
|
|
|
|
// We haven't found it yet, let's look in the identities
|
|
// associated to this user
|
|
if (!p)
|
|
{
|
|
SOGoUserManager *sm;
|
|
NSString *uid;
|
|
|
|
sm = [SOGoUserManager sharedUserManager];
|
|
uid = [sm getUIDForEmail: baseEmail];
|
|
|
|
if (uid)
|
|
{
|
|
NSArray *allEmails;
|
|
NSString *email;
|
|
SOGoUser *u;
|
|
int i;
|
|
|
|
u = [SOGoUser userWithLogin: uid];
|
|
allEmails = [u allEmails];
|
|
for (i = 0; i < [allEmails count]; i++)
|
|
{
|
|
email = [allEmails objectAtIndex: i];
|
|
if ([email caseInsensitiveCompare: baseEmail] == NSOrderedSame)
|
|
continue;
|
|
|
|
p = [e findAttendeeWithEmail: email];
|
|
|
|
if (p)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
- (iCalPerson *) storedReplyAttendee
|
|
{
|
|
iCalEvent *e;
|
|
|
|
e = [self storedEvent];
|
|
if (e)
|
|
return [self _attendeeFromMailSenderInEvent: e];
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (BOOL) isReplySenderAnAttendee
|
|
{
|
|
return ([self storedReplyAttendee] != nil);
|
|
}
|
|
|
|
- (iCalPerson *) _emailParticipantWithEvent: (iCalEvent *) event
|
|
{
|
|
return [self _attendeeFromMailSenderInEvent: event];
|
|
}
|
|
|
|
- (BOOL) hasSenderStatusChanged
|
|
{
|
|
iCalPerson *emailParticipant, *calendarParticipant;
|
|
|
|
[self inEvent];
|
|
[self storedEvent];
|
|
emailParticipant = [self _emailParticipantWithEvent: inEvent];
|
|
calendarParticipant = [self _emailParticipantWithEvent: storedEvent];
|
|
|
|
return ([[emailParticipant partStat]
|
|
caseInsensitiveCompare: [calendarParticipant partStat]]
|
|
!= NSOrderedSame);
|
|
}
|
|
|
|
- (BOOL) canOriginalEventBeUpdated
|
|
{
|
|
return ([self hasCalendarAccess]
|
|
&& [self hasSenderStatusChanged]
|
|
&& ([[inEvent sequence] compare: [storedEvent sequence]]
|
|
!= NSOrderedAscending));
|
|
}
|
|
|
|
- (id) renderedPart
|
|
{
|
|
NSMutableDictionary *d;
|
|
NSArray *participants;
|
|
iCalPerson *person;
|
|
NSMutableArray *a;
|
|
int i;
|
|
|
|
d = [NSMutableDictionary dictionaryWithDictionary: [super renderedPart]];
|
|
|
|
// We also add our participants
|
|
participants = [[self authorativeEvent] participants];
|
|
a = [NSMutableArray array];
|
|
|
|
for (i = 0; i < [participants count]; i++)
|
|
{
|
|
person = [participants objectAtIndex: i];
|
|
|
|
if (![[person delegatedTo] length])
|
|
[a addObject: [NSDictionary dictionaryWithObjectsAndKeys: ([[person cnWithoutQuotes] length] ? [person cnWithoutQuotes] : [person rfc822Email]), @"name",
|
|
[person rfc822Email], @"email",
|
|
[[person partStatWithDefault] lowercaseString], @"partstat", nil]];
|
|
}
|
|
|
|
[d setObject: a forKey: @"participants"];
|
|
|
|
//Sanitise the html content
|
|
if([d objectForKey:@"content"]){
|
|
[d setObject: [[d objectForKey:@"content"] stringWithoutHTMLInjection: NO stripAngular:NO] forKey: @"content"];
|
|
}
|
|
|
|
return d;
|
|
}
|
|
|
|
- (BOOL) hasLocation {
|
|
return [self inEvent] != nil
|
|
&& [[self inEvent] location] != nil
|
|
&& [[[self inEvent] location] length] > 0;
|
|
}
|
|
|
|
- (NSString *) location {
|
|
return [[self inEvent] location];
|
|
}
|
|
|
|
- (NSString *) userComment
|
|
{
|
|
return [[[self inEvent] userComment] stringByEscapingHTMLString];
|
|
}
|
|
|
|
- (NSString *) eventDescription
|
|
{
|
|
return [[[self authorativeEvent] comment] stringByEscapingHTMLString];
|
|
}
|
|
|
|
@end /* UIxMailPartICalViewer */
|