diff --git a/NEWS b/NEWS index e20e9245a..a2e31e7c3 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,9 @@ 3.2.5 (2016-12-DD) ------------------ +New features + - [web] download attachments of a message as a zip archive + Enhancements - [web] prevent using localhost on additional IMAP accounts diff --git a/SoObjects/Mailer/NSString+Mail.h b/SoObjects/Mailer/NSString+Mail.h index 6398f136e..7415f55e5 100644 --- a/SoObjects/Mailer/NSString+Mail.h +++ b/SoObjects/Mailer/NSString+Mail.h @@ -33,6 +33,7 @@ fromIndex: (int) start; - (int) indexOf: (unichar) _c; - (NSString *) decodedHeader; +- (NSString *) asSafeFilename; @end diff --git a/SoObjects/Mailer/NSString+Mail.m b/SoObjects/Mailer/NSString+Mail.m index 523953311..47c7edaf4 100644 --- a/SoObjects/Mailer/NSString+Mail.m +++ b/SoObjects/Mailer/NSString+Mail.m @@ -48,11 +48,11 @@ @interface _SOGoHTMLContentHandler : NSObject { NSMutableArray *images; - + NSArray *ignoreContentTags; NSArray *specialTreatmentTags; NSArray *voidTags; - + BOOL ignoreContent; BOOL orderedList; BOOL unorderedList; @@ -300,7 +300,7 @@ i = [value indexOf: ';']; j = [value indexOf: ';' fromIndex: i+1]; k = [value indexOf: ',']; - + // We try to get the MIME type mimeType = nil; @@ -314,7 +314,7 @@ // We might get a stupid value. We discard anything that doesn't have a / in it if ([mimeType indexOf: '/'] < 0) mimeType = @"image/jpeg"; - + // We check and skip the charset if (j < i) j = i; @@ -335,14 +335,14 @@ [map setObject: [NSString stringWithFormat: @"inline; filename=\"%@\"", uniqueId] forKey: @"content-disposition"]; [map setObject: [NSString stringWithFormat: @"%@; name=\"%@\"", mimeType, uniqueId] forKey: @"content-type"]; [map setObject: [NSString stringWithFormat: @"<%@>", uniqueId] forKey: @"content-id"]; - - + + body = [[NGMimeFileData alloc] initWithBytes: [data bytes] length: [data length]]; bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; [bodyPart setBody: body]; [body release]; - + [images addObject: bodyPart]; [result appendFormat: @""]; } } @@ -656,19 +656,19 @@ convertChars (const char *oldString, unsigned int oldLength, fromIndex: (int) start { int i, len; - + len = [self length]; - + if (start < 0 || start >= len) start = 0; - + for (i = start; i < len; i++) { if ([self characterAtIndex: i] == _c) return i; } return -1; - + } - (int) indexOf: (unichar) _c @@ -684,8 +684,30 @@ convertChars (const char *oldString, unsigned int oldLength, decodedHeader]; if (!decodedHeader) decodedHeader = self; - + return decodedHeader; } +- (NSString *) asSafeFilename +{ + NSRange r; + NSMutableString *safeName; + + r = [self rangeOfString: @"\\" + options: NSBackwardsSearch]; + if (r.length > 0) + safeName = [NSMutableString stringWithString: [self substringFromIndex: r.location + 1]]; + else + safeName = [NSMutableString stringWithString: self]; + [safeName replaceString: @"/" withString: @"_"]; + + if ([self isEqualToString: @"."]) + return @"_"; + + if ([self isEqualToString: @".."]) + return @"__"; + + return safeName; +} + @end diff --git a/SoObjects/Mailer/SOGoDraftObject.h b/SoObjects/Mailer/SOGoDraftObject.h index cf4e06d27..2d3bd9610 100644 --- a/SoObjects/Mailer/SOGoDraftObject.h +++ b/SoObjects/Mailer/SOGoDraftObject.h @@ -97,7 +97,6 @@ /* attachments */ - (NSArray *) fetchAttachmentAttrs; -- (BOOL) isValidAttachmentName: (NSString *) _name; - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name; - (NSString *) pathToAttachmentWithName: (NSString *) _name; - (NSException *) saveAttachment: (NSData *) _attach diff --git a/SoObjects/Mailer/SOGoDraftObject.m b/SoObjects/Mailer/SOGoDraftObject.m index c31239379..8b518620f 100644 --- a/SoObjects/Mailer/SOGoDraftObject.m +++ b/SoObjects/Mailer/SOGoDraftObject.m @@ -74,7 +74,7 @@ static NSString *contentTypeValue = @"text/plain; charset=utf-8"; static NSString *htmlContentTypeValue = @"text/html; charset=utf-8"; -static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", +static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", @"from", @"replyTo", @"message-id", nil}; @@ -142,18 +142,18 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", { const char *b, *bytes; int i, len, slen; - + if (!theCString) { return NSMakeRange(NSNotFound,0); } - + bytes = [self bytes]; len = [self length]; slen = strlen(theCString); - + b = bytes; - + if (len > theRange.location + theRange.length) { len = theRange.location + theRange.length; @@ -163,7 +163,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", { i = theRange.location; b += i; - + for (; i <= len-slen; i++, b++) { if (!strncasecmp(theCString,b,slen)) @@ -176,7 +176,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", { i = theRange.location; b += i; - + for (; i <= len-slen; i++, b++) { if (!memcmp(theCString,b,slen)) @@ -185,7 +185,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", } } } - + return NSMakeRange(NSNotFound,0); } @@ -271,7 +271,7 @@ static NSString *userAgent = nil; NSFileManager *fm; fm = [NSFileManager defaultManager]; - + return ([fm createDirectoriesAtPath: [container userSpoolFolderPath] attributes: nil] && [fm createDirectoriesAtPath: [self draftFolderPath] @@ -308,7 +308,7 @@ static NSString *userAgent = nil; messageID = [NSString generateMessageID]; [headers setObject: messageID forKey: @"message-id"]; } - + priority = [newHeaders objectForKey: @"X-Priority"]; if (priority) { @@ -649,7 +649,7 @@ static NSString *userAgent = nil; atURL: [[self mailAccountFolder] imap4URL]]; [imap4 flushFolderHierarchyCache]; } - + folder = [imap4 imap4FolderNameForURL: [container imap4URL]]; result = [client append: message toFolder: folder withFlags: [NSArray arrayWithObjects: @"draft", nil]]; @@ -745,7 +745,7 @@ static NSString *userAgent = nil; - if there is a 'reply-to' header, only include that (as TO) - if we reply to all, all non-from addresses are added as CC - the from is always the lone TO (except for reply-to) - + Note: we cannot check reply-to, because Cyrus even sets a reply-to in the envelope if none is contained in the message itself! (bug or feature?) @@ -776,11 +776,11 @@ static NSString *userAgent = nil; int i; identities = [[[self container] mailAccountFolder] identities]; - + for (i = 0; i < [identities count]; i++) { email = [[identities objectAtIndex: i] objectForKey: @"email"]; - + if (email) [allRecipients addObject: email]; } @@ -838,66 +838,29 @@ static NSString *userAgent = nil; [self _purgeRecipients: allRecipients fromAddresses: addrs]; [self _addEMailsOfAddresses: addrs toArray: to]; - + [_info setObject: to forKey: @"cc"]; [to release]; } } -// -// -// -- (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths - fromResponseFetch: (NSDictionary *) fetch; -{ - NSEnumerator *attachmentKeys; - NSMutableArray *bodies; - NSString *currentKey; - NSDictionary *body; - - bodies = [NSMutableArray array]; - - attachmentKeys = [paths objectEnumerator]; - while ((currentKey = [attachmentKeys nextObject])) - { - body = [fetch objectForKey: [currentKey lowercaseString]]; - if (body) - [bodies addObject: [body objectForKey: @"data"]]; - else - [bodies addObject: [NSData data]]; - } - - return bodies; -} - // // // - (void) _fetchAttachmentsFromMail: (SOGoMailObject *) sourceMail { - unsigned int count, max; - NSArray *parts, *paths, *bodies; - NSData *body; + unsigned int max, count; + NSArray *attachments; NSDictionary *currentInfo; - NGHashMap *response; - parts = [sourceMail fetchFileAttachmentKeys]; - max = [parts count]; - if (max > 0) + attachments = [sourceMail fetchFileAttachments]; + max = [attachments count]; + for (count = 0; count < max; count++) { - paths = [parts keysWithFormat: @"BODY[%{path}]"]; - response = [[sourceMail fetchParts: paths] objectForKey: @"RawResponse"]; - bodies = [self _attachmentBodiesFromPaths: paths - fromResponseFetch: [response objectForKey: @"fetch"]]; - for (count = 0; count < max; count++) - { - currentInfo = [parts objectAtIndex: count]; - body = [[bodies objectAtIndex: count] - bodyDataFromEncoding: [currentInfo - objectForKey: @"encoding"]]; - [self saveAttachment: body withMetadata: currentInfo]; - } + currentInfo = [attachments objectAtIndex: count]; + [self saveAttachment: [currentInfo objectForKey: @"body"] + withMetadata: currentInfo]; } } @@ -1002,14 +965,14 @@ static NSString *userAgent = nil; SOGoUserDefaults *ud; [sourceMail fetchCoreInfos]; - + if ([sourceMail subjectForForward]) { - info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward] + info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward] forKey: @"subject"]; [self setHeaders: info]; } - + [self setSourceURL: [sourceMail imap4URLString]]; [self setSourceFlag: @"$Forwarded"]; [self setSourceIMAP4ID: [[sourceMail nameInContainer] intValue]]; @@ -1052,7 +1015,7 @@ static NSString *userAgent = nil; - (NSString *) sender { id tmp; - + if ((tmp = [headers objectForKey: @"from"]) == nil) return nil; if ([tmp isKindOfClass:[NSArray class]]) @@ -1098,52 +1061,36 @@ static NSString *userAgent = nil; return ma; } -- (BOOL) isValidAttachmentName: (NSString *) filename -{ - return (!([filename rangeOfString: @"/"].length - || [filename isEqualToString: @"."] - || [filename isEqualToString: @".."])); -} - (NSString *) pathToAttachmentWithName: (NSString *) _name { if ([_name length] == 0) return nil; - + return [[self draftFolderPath] stringByAppendingPathComponent:_name]; } -- (NSException *) invalidAttachmentNameError: (NSString *) _name -{ - return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ - reason: @"Invalid attachment name!"]; -} +/** + * Write attachment file to the spool directory of the draft and write a dot + * file with its mime type. + */ - (NSException *) saveAttachment: (NSData *) _attach withMetadata: (NSDictionary *) metadata { NSString *p, *pmime, *name, *mimeType; - NSRange r; if (![_attach isNotNull]) { return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ reason: @"Missing attachment content!"]; } - + if (![self _ensureDraftFolderPath]) { return [NSException exceptionWithHTTPStatus:500 /* Server Error */ reason: @"Could not create folder for draft!"]; } - name = [metadata objectForKey: @"filename"]; - r = [name rangeOfString: @"\\" - options: NSBackwardsSearch]; - if (r.length > 0) - name = [name substringFromIndex: r.location + 1]; - - if (![self isValidAttachmentName: name]) - return [self invalidAttachmentNameError: name]; - + name = [[metadata objectForKey: @"filename"] asSafeFilename]; p = [self pathToAttachmentWithName: name]; if (![_attach writeToFile: p atomically: YES]) { @@ -1162,7 +1109,7 @@ static NSString *userAgent = nil; reason: @"Could not write attachment to draft!"]; } } - + return nil; /* everything OK */ } @@ -1173,21 +1120,14 @@ static NSString *userAgent = nil; NSException *error; error = nil; + fm = [NSFileManager defaultManager]; + p = [self pathToAttachmentWithName: [_name asSafeFilename]]; + if ([fm fileExistsAtPath: p]) + if (![fm removeFileAtPath: p handler: nil]) + error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ + reason: @"Could not delete attachment from draft!"]; - if ([self isValidAttachmentName:_name]) - { - fm = [NSFileManager defaultManager]; - p = [self pathToAttachmentWithName:_name]; - if ([fm fileExistsAtPath: p]) - if (![fm removeFileAtPath: p handler: nil]) - error - = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ - reason: @"Could not delete attachment from draft!"]; - } - else - error = [self invalidAttachmentNameError:_name]; - - return error; + return error; } // @@ -1201,9 +1141,9 @@ static NSString *userAgent = nil; /* prepare header of body part */ map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease]; - + [map setObject: contentTypeValue forKey: @"content-type"]; - + /* prepare body content */ bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; @@ -1224,7 +1164,7 @@ static NSString *userAgent = nil; */ NGMutableHashMap *map; NGMimeBodyPart *bodyPart; - + /* prepare header of body part */ map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease]; @@ -1232,7 +1172,7 @@ static NSString *userAgent = nil; if (text) [map setObject: (isHTML ? htmlContentTypeValue : contentTypeValue) forKey: @"content-type"]; - + /* prepare body content */ bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; [bodyPart setBody: text]; @@ -1242,11 +1182,11 @@ static NSString *userAgent = nil; - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map { - NGMimeMessage *message; + NGMimeMessage *message; id body; message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease]; - + if (!isHTML) { [message setHeader: contentTypeValue forKey: @"content-type"]; @@ -1263,9 +1203,9 @@ static NSString *userAgent = nil; // Add the HTML part [body addBodyPart: [self bodyPartForText]]; } - + [message setBody: body]; - + return message; } @@ -1287,9 +1227,8 @@ static NSString *userAgent = nil; { NSString *s, *p; NSData *mimeData; - - p = [self pathToAttachmentWithName: - [NSString stringWithFormat: @".%@.mime", _name]]; + + p = [self pathToAttachmentWithName: [NSString stringWithFormat: @".%@.mime", _name]]; mimeData = [NSData dataWithContentsOfFile: p]; if (mimeData) { @@ -1308,20 +1247,18 @@ static NSString *userAgent = nil; } - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name + andContentType: (NSString *) _type { - NSString *type; NSString *cdtype; NSString *cd; SOGoDomainDefaults *dd; - - type = [self contentTypeForAttachmentWithName:_name]; - if ([type hasPrefix: @"text/"]) + if ([_type hasPrefix: @"text/"]) { dd = [[context activeUser] domainDefaults]; cdtype = [dd mailAttachTextDocumentsInline] ? @"inline" : @"attachment"; } - else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"]) + else if ([_type hasPrefix: @"image/"] || [_type hasPrefix: @"message"]) cdtype = @"inline"; else cdtype = @"attachment"; @@ -1348,7 +1285,7 @@ static NSString *userAgent = nil; if (_name == nil) return nil; /* check attachment */ - + fm = [NSFileManager defaultManager]; p = [self pathToAttachmentWithName: _name]; if (![fm isReadableFileAtPath: p]) { @@ -1357,7 +1294,7 @@ static NSString *userAgent = nil; } attachAsString = NO; attachAsRFC822 = NO; - + /* prepare header of body part */ map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease]; @@ -1369,22 +1306,22 @@ static NSString *userAgent = nil; else if ([s hasPrefix: @"message/rfc822"]) attachAsRFC822 = YES; } - if ((s = [self contentDispositionForAttachmentWithName: _name])) + if ((s = [self contentDispositionForAttachmentWithName: _name andContentType: s])) { NGMimeContentDispositionHeaderField *o; - + o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s]; [map setObject: o forKey: @"content-disposition"]; [o release]; } - + /* prepare body content */ - + if (attachAsString) { // TODO: is this really necessary? NSString *s; - + content = [[NSData alloc] initWithContentsOfMappedFile:p]; - + s = [[NSString alloc] initWithData: content encoding: [NSString defaultCStringEncoding]]; if (s != nil) { @@ -1399,7 +1336,7 @@ static NSString *userAgent = nil; } } else { - /* + /* Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently NGMimeFileData objects are not processed by the MIME generator! */ @@ -1415,17 +1352,17 @@ static NSString *userAgent = nil; content = [content dataByEncodingBase64]; [map setObject: @"base64" forKey: @"content-transfer-encoding"]; } - [map setObject:[NSNumber numberWithInt:[content length]] + [map setObject:[NSNumber numberWithInt:[content length]] forKey: @"content-length"]; - + /* Note: the -init method will create a temporary file! */ body = [[NGMimeFileData alloc] initWithBytes:[content bytes] length:[content length]]; } - + bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; [bodyPart setBody:body]; - + [body release]; body = nil; return bodyPart; } @@ -1462,14 +1399,14 @@ static NSString *userAgent = nil; NGMimeMultipartBody *textParts; NGMutableHashMap *header; NGMimeBodyPart *part; - + header = [NGMutableHashMap hashMap]; [header addObject: MultiAlternativeType forKey: @"content-type"]; - + part = [NGMimeBodyPart bodyPartWithHeader: header]; - + textParts = [[NGMimeMultipartBody alloc] initWithPart: part]; - + // Get the text part from it and add it [textParts addBodyPart: [self plainTextBodyPartForText]]; @@ -1488,17 +1425,17 @@ static NSString *userAgent = nil; - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map andBodyParts: (NSArray *) _bodyParts { - NGMimeMessage *message; + NGMimeMessage *message; NGMimeMultipartBody *mBody; NSEnumerator *e; id part; - + [map addObject: MultiMixedType forKey: @"content-type"]; message = [[NGMimeMessage alloc] initWithHeader: map]; [message autorelease]; mBody = [[NGMimeMultipartBody alloc] initWithPart: message]; - + if (!isHTML) { part = [self bodyPartForText]; @@ -1535,11 +1472,11 @@ static NSString *userAgent = nil; if ([_h count] == 0) return; - + names = [_h keyEnumerator]; while ((name = [names nextObject]) != nil) { id value; - + value = [_h objectForKey:name]; [_map addObject:value forKey:name]; } @@ -1549,10 +1486,10 @@ static NSString *userAgent = nil; { if (![_value isNotNull]) return YES; - + if ([_value isKindOfClass: [NSArray class]]) return [_value count] == 0 ? YES : NO; - + if ([_value isKindOfClass: [NSString class]]) return [_value length] == 0 ? YES : NO; @@ -1630,9 +1567,9 @@ static NSString *userAgent = nil; NSString *s, *dateString; NGMutableHashMap *map; id emails, from, replyTo; - + map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease]; - + /* add recipients */ if ((emails = [headers objectForKey: @"to"]) != nil && [emails isKindOfClass: [NSArray class]]) [map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"to"]; @@ -1643,7 +1580,7 @@ static NSString *userAgent = nil; /* add senders */ from = [headers objectForKey: @"from"]; - + if (![self isEmptyValue:from]) { if ([from isKindOfClass:[NSArray class]]) [map setObjects: [self _quoteSpecialsInArray: from] forKey: @"from"]; @@ -1661,7 +1598,7 @@ static NSString *userAgent = nil; if ([(s = [headers objectForKey: @"subject"]) length] > 0) [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"]; - + if ([(s = [headers objectForKey: @"message-id"]) length] > 0) [map setObject: s forKey: @"message-id"]; @@ -1685,16 +1622,16 @@ static NSString *userAgent = nil; forKey: @"Disposition-Notification-To"]; [self _addHeaders: _headers toHeaderMap: map]; - + // We remove what we have to... if (_exclude) { int i; - + for (i = 0; i < [_exclude count]; i++) [map removeAllObjectsForKey: [_exclude objectAtIndex: i]]; } - + return map; } @@ -1730,11 +1667,11 @@ static NSString *userAgent = nil; if (map) { //[self debugWithFormat: @"MIME Envelope: %@", map]; - + [bodyParts addObjectsFromArray: [self bodyPartsForAllAttachments]]; - + //[self debugWithFormat: @"attachments: %@", bodyParts]; - + if ([bodyParts count] == 0) /* no attachments */ message = [self mimeMessageForContentWithHeaderMap: map]; @@ -1748,13 +1685,13 @@ static NSString *userAgent = nil; [map addObject: MultiRelatedType forKey: @"content-type"]; } - message = [self mimeMultiPartMessageWithHeaderMap: map + message = [self mimeMultiPartMessageWithHeaderMap: map andBodyParts: bodyParts]; //[self debugWithFormat: @"message: %@", message]; } } - + return message; } @@ -1827,9 +1764,9 @@ static NSString *userAgent = nil; - (NSException *) sendMail { SOGoUserDefaults *ud; - + ud = [[context activeUser] userDefaults]; - + if ([ud mailAddOutgoingAddresses]) { NSString *recipient, *emailAddress, *addressBook, *uid; @@ -1840,9 +1777,9 @@ static NSString *userAgent = nil; NGMailAddressParser *parser; SOGoFolder *folder; NGVCard *card; - + int i; - + // Get all the addressbooks contactFolders = [[[context activeUser] homeFolderInContext: context] lookupName: @"Contacts" @@ -1857,7 +1794,7 @@ static NSString *userAgent = nil; parser = [NGMailAddressParser mailAddressParserWithString: recipient]; parsedRecipient = [parser parse]; emailAddress = [parsedRecipient address]; - + matchingContacts = [contactFolders allContactsFromFilter: emailAddress excludeGroups: YES excludeLists: YES]; @@ -1869,13 +1806,13 @@ static NSString *userAgent = nil; addressBook = [ud selectedAddressBook]; folder = [contactFolders lookupName: addressBook inContext: context acquire: NO]; uid = [folder globallyUniqueObjectId]; - + if (folder && uid) { card = [NGVCard cardWithUid: uid]; [card addEmail: emailAddress types: nil]; [card setFn: [parsedRecipient displayName]]; - + newContact = [SOGoContactGCSEntry objectWithName: uid inContainer: folder]; [newContact setIsNew: YES]; @@ -1921,7 +1858,7 @@ static NSString *userAgent = nil; r1 = [cleaned_message rangeOfCString: "\r\nbcc: " options: 0 range: NSMakeRange(0,limit)]; - + if (r1.location != NSNotFound) { // We search for the first \r\n AFTER the Bcc: header and diff --git a/SoObjects/Mailer/SOGoMailObject.h b/SoObjects/Mailer/SOGoMailObject.h index 8089bcf0b..f3a70642f 100644 --- a/SoObjects/Mailer/SOGoMailObject.h +++ b/SoObjects/Mailer/SOGoMailObject.h @@ -1,4 +1,5 @@ /* + Copyright (C) 2009-2016 Inverse inc. Copyright (C) 2004-2005 SKYRIX Software AG This file is part of OpenGroupware.org. @@ -48,6 +49,8 @@ @class NGImap4Envelope; @class NGImap4EnvelopeAddress; +@class WOResponse; + NSArray *SOGoMailCoreInfoKeys; @interface SOGoMailObject : SOGoMailBaseObject @@ -107,6 +110,8 @@ NSArray *SOGoMailCoreInfoKeys; - (BOOL) hasAttachment; - (NSDictionary *) fetchFileAttachmentIds; - (NSArray *) fetchFileAttachmentKeys; +- (NSArray *) fetchFileAttachments; +- (WOResponse *) archiveAllFilesinArchiveNamed: (NSString *) archiveName; /* flags */ diff --git a/SoObjects/Mailer/SOGoMailObject.m b/SoObjects/Mailer/SOGoMailObject.m index d38196f70..41947964a 100644 --- a/SoObjects/Mailer/SOGoMailObject.m +++ b/SoObjects/Mailer/SOGoMailObject.m @@ -1,5 +1,5 @@ /* - Copyright (C) 2007-2014 Inverse inc. + Copyright (C) 2007-2016 Inverse inc. Copyright (C) 2004-2005 SKYRIX Software AG This file is part of SOGo. @@ -20,6 +20,7 @@ 02111-1307, USA. */ +#import #import #import @@ -27,6 +28,7 @@ #import #import #import +#import #import #import #import @@ -38,7 +40,9 @@ #import #import +#import #import +#import #import #import #import @@ -122,10 +126,10 @@ static BOOL debugSoParts = NO; - (NSString *) keyExtensionForPart: (id) _partInfo { NSString *mt, *st; - + if (_partInfo == nil) return nil; - + mt = [_partInfo valueForKey: @"type"]; st = [[_partInfo valueForKey: @"subtype"] lowercaseString]; if ([mt isEqualToString: @"text"]) { @@ -140,7 +144,7 @@ static BOOL debugSoParts = NO; if ([st isEqualToString: @"pgp-signature"]) return @".asc"; } - + return nil; } @@ -149,18 +153,18 @@ static BOOL debugSoParts = NO; NSMutableArray *ma; NSArray *parts; unsigned i, count; - + parts = [[self bodyStructure] valueForKey: @"parts"]; - if (![parts isNotNull]) + if (![parts isNotNull]) return nil; if ((count = [parts count]) == 0) return nil; - + for (i = 0, ma = nil; i < count; i++) { NSString *key, *ext; id part; BOOL hasParts; - + part = [parts objectAtIndex:i]; hasParts = [part valueForKey: @"parts"] != nil ? YES:NO; if ((hasParts && !_withParts) || (_withParts && !hasParts)) @@ -168,7 +172,7 @@ static BOOL debugSoParts = NO; if (ma == nil) ma = [NSMutableArray arrayWithCapacity:count - i]; - + ext = [self keyExtensionForPart:part]; key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ((id)ext?(id)ext: (id)@"")]; [ma addObject:key]; @@ -204,15 +208,15 @@ static BOOL debugSoParts = NO; { static NSArray *existsKey = nil; id msgs; - + if (coreInfos != nil) /* if we have coreinfos, we can use them */ return [coreInfos isNotNull]; - + /* otherwise fetch something really simple */ - + if (existsKey == nil) /* we use size, other suggestions? */ existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil]; - + msgs = [self fetchParts:existsKey]; // returns dict msgs = [msgs valueForKey: @"fetch"]; return [msgs count] > 0 ? YES : NO; @@ -229,7 +233,7 @@ static BOOL debugSoParts = NO; if (heavyDebug) [self logWithFormat: @"M: %@", msgs]; msgs = [msgs valueForKey: @"fetch"]; - + // We MUST honor untagged IMAP responses here otherwise we could // return really borken and nasty results. if ([msgs count] > 0) @@ -237,7 +241,7 @@ static BOOL debugSoParts = NO; for (i = 0; i < [msgs count]; i++) { coreInfos = [msgs objectAtIndex: i]; - + if ([[coreInfos objectForKey: @"uid"] intValue] == [[self nameInContainer] intValue]) break; @@ -337,13 +341,13 @@ static BOOL debugSoParts = NO; { NGMimeMessageParser *parser; NSData *data; - + if (headerPart != nil) return [headerPart isNotNull] ? headerPart : nil; - + if ([(data = [self mailHeaderData]) length] == 0) return nil; - + // TODO: do we need to set some delegate method which stops parsing the body? parser = [[NGMimeMessageParser alloc] init]; headerPart = [[parser parsePartFromData:data] retain]; @@ -372,21 +376,21 @@ static BOOL debugSoParts = NO; if (![_path isNotNull]) return nil; - + if ((info = [self bodyStructure]) == nil) { [self errorWithFormat: @"got no body part structure!"]; return nil; } /* ensure array argument */ - + if ([_path isKindOfClass:[NSString class]]) { if ([_path length] == 0 || [_path isEqualToString: @"text"]) return info; - + _path = [_path componentsSeparatedByString: @"."]; } - + // deal with mails of type text/calendar if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"text"] && [[[info valueForKey: @"subtype"] lowercaseString] isEqualToString: @"calendar"]) @@ -399,12 +403,12 @@ static BOOL debugSoParts = NO; if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"application"]) return info; - /* - For each path component, eg 1,1,3 - + /* + For each path component, eg 1,1,3 + Remember that we need special processing for message/rfc822 which maps the namespace of multiparts directly into the main namespace. - + TODO(hh): no I don't remember, please explain in more detail! */ pe = [_path objectEnumerator]; @@ -412,7 +416,7 @@ static BOOL debugSoParts = NO; unsigned idx; NSArray *parts; NSString *mt; - + [self debugWithFormat: @"check PATH: %@", p]; idx = [p intValue] - 1; @@ -421,7 +425,7 @@ static BOOL debugSoParts = NO; if ([mt isEqualToString: @"message"]) { /* we have special behaviour for message types */ id body; - + if ((body = [info valueForKey: @"body"]) != nil) { mt = [body valueForKey: @"type"]; if ([mt isEqualToString: @"multipart"]) @@ -430,10 +434,10 @@ static BOOL debugSoParts = NO; parts = [NSArray arrayWithObject:body]; } } - + if (idx >= [parts count]) { [self errorWithFormat: - @"body part index out of bounds(idx=%d vs count=%d): %@", + @"body part index out of bounds(idx=%d vs count=%d): %@", (idx + 1), [parts count], info]; return nil; } @@ -448,40 +452,40 @@ static BOOL debugSoParts = NO; { NSData *content; id result, fullResult; - + // We avoid using RFC822 here as the part name as it'll flag the message as Seen fullResult = [self fetchParts: [NSArray arrayWithObject: @"BODY.PEEK[]"]]; if (fullResult == nil) return nil; - + if ([fullResult isKindOfClass: [NSException class]]) return fullResult; - + /* extract fetch result */ - + result = [fullResult valueForKey: @"fetch"]; if (![result isKindOfClass:[NSArray class]]) { [self logWithFormat: - @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", + @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", fullResult]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason: @"unexpected IMAP4 result"]; } if ([result count] == 0) return nil; - + result = [result objectAtIndex:0]; - + /* extract message */ - + if ((content = [[result valueForKey: @"body[]"] valueForKey: @"data"]) == nil) { [self logWithFormat: - @"ERROR: unexpected IMAP4 result (missing 'message'): %@", + @"ERROR: unexpected IMAP4 result (missing 'message'): %@", result]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason: @"unexpected IMAP4 result"]; } - + return [[content copy] autorelease]; } @@ -507,7 +511,7 @@ static BOOL debugSoParts = NO; [s autorelease]; else [self logWithFormat: - @"ERROR: could not convert data of length %d to string", + @"ERROR: could not convert data of length %d to string", [content length]]; } else @@ -529,11 +533,11 @@ static BOOL debugSoParts = NO; withPeek: (BOOL) withPeek parentMultipart: (NSString *) parentMPart { - /* + /* This is used to collect the set of IMAP4 fetch-keys required to fetch the basic parts of the body structure. That is, to fetch all parts which are displayed 'inline' in a single IMAP4 fetch. - + The method calls itself recursively to walk the body structure. */ NSArray *parts; @@ -555,7 +559,7 @@ static BOOL debugSoParts = NO; multipart = mimeType; else multipart = parentMPart; - + if ([types containsObject: mimeType]) { if ([p length] > 0) @@ -581,9 +585,9 @@ static BOOL debugSoParts = NO; sp = (([p length] > 0) ? (id)[p stringByAppendingFormat: @".%d", i + 1] : (id)[NSString stringWithFormat: @"%d", i + 1]); - + childInfo = [parts objectAtIndex: i]; - + [self addRequiredKeysOfStructure: childInfo path: sp toArray: keys @@ -591,7 +595,7 @@ static BOOL debugSoParts = NO; withPeek: withPeek parentMultipart: multipart]; } - + /* check body */ body = [info objectForKey: @"body"]; if (body) @@ -651,7 +655,7 @@ static BOOL debugSoParts = NO; [self addRequiredKeysOfStructure: [self bodyStructure] path: @"" - toArray: ma + toArray: ma acceptedTypes: types withPeek: YES]; @@ -665,7 +669,7 @@ static BOOL debugSoParts = NO; unsigned i, count; NSArray *results; id result; - + [self debugWithFormat: @"fetch keys: %@", _fetchKeys]; result = [self fetchParts: [_fetchKeys objectsForKey: @"key" @@ -681,7 +685,7 @@ static BOOL debugSoParts = NO; for (i = 0; i < count; i++) { NSString *key; NSData *data; - + key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"]; // We'll ask for the body.peek[] but SOPE returns us body[] responses @@ -689,19 +693,19 @@ static BOOL debugSoParts = NO; if ([key hasPrefix: @"body.peek["]) key = [NSString stringWithFormat: @"body[%@", [key substringFromIndex: 10]]; - data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] + data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] objectForKey: @"data"]; - + if (![data isNotNull]) { [self errorWithFormat: @"got no data for key: %@", key]; continue; } - + if ([key isEqualToString: @"body[text]"]) key = @""; // see key collector for explanation (TODO: where?) else if ([key hasPrefix: @"body["]) { NSRange r; - + key = [key substringFromIndex:5]; r = [key rangeOfString: @"]"]; if (r.length > 0) @@ -913,13 +917,167 @@ static BOOL debugSoParts = NO; return keys; } +/** + * Returns an array of dictionaries with the following keys: + * - encoding + * - filename + * - mimetype + * - path + * - size + * - url + * - urlAsAttachment + * - body (NSData) + */ +- (NSArray *) fetchFileAttachments +{ + unsigned int count, max; + NGHashMap *response; + NSArray *parts, *paths; //, *bodies; + NSData *body; + NSDictionary *fetch, *currentInfo, *currentBody; + NSMutableArray *attachments; + NSMutableDictionary *currentAttachment; + NSString *currentPath; + + parts = [self fetchFileAttachmentKeys]; + max = [parts count]; + attachments = [NSMutableArray arrayWithCapacity: max]; + if (max > 0) + { + paths = [parts keysWithFormat: @"BODY[%{path}]"]; + response = [[self fetchParts: paths] objectForKey: @"RawResponse"]; + fetch = [response objectForKey: @"fetch"]; + for (count = 0; count < max; count++) + { + currentInfo = [parts objectAtIndex: count]; + currentPath = [[paths objectAtIndex: count] lowercaseString]; + currentBody = [fetch objectForKey: currentPath]; + + if (currentBody) + { + body = [currentBody objectForKey: @"data"]; + body = [body bodyDataFromEncoding: [currentInfo objectForKey: @"encoding"]]; + } + else + body = [NSData data]; + + currentAttachment = [NSMutableDictionary dictionaryWithDictionary: currentInfo]; + [currentAttachment setObject: body forKey: @"body"]; + [attachments addObject: currentAttachment]; + } + } + + return attachments; +} + +- (WOResponse *) archiveAllFilesinArchiveNamed: (NSString *) archiveName +{ + NSArray *attachments; + NSData *body, *zipContent; + NSDictionary *currentAttachment; + NSException *error; + NSFileManager *fm; + NSMutableArray *zipTaskArguments; + NSString *spoolPath, *name, *fileName, *baseName, *extension, *zipPath, *qpFileName; + NSTask *zipTask; + SOGoMailFolder *folder; + WOResponse *response; + unsigned int max, count; + + if (!archiveName) + archiveName = @"attachments.zip"; + + folder = [self container]; + spoolPath = [folder userSpoolFolderPath]; + + if (![folder ensureSpoolFolderPath]) + { + [self errorWithFormat: @"spool directory '%@' doesn't exist", spoolPath]; + error = [NSException exceptionWithHTTPStatus: 500 + reason: @"spool directory does not exist"]; + return (WOResponse *)error; + } + + // Prepare execution of zip + zipPath = [[SOGoSystemDefaults sharedSystemDefaults] zipPath]; + fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath: zipPath]) + { + error = [NSException exceptionWithHTTPStatus: 500 + reason: @"zip not available"]; + return (WOResponse *)error; + } + + zipTask = [[NSTask alloc] init]; + [zipTask setCurrentDirectoryPath: spoolPath]; + [zipTask setLaunchPath: zipPath]; + + zipTaskArguments = [NSMutableArray arrayWithObjects: nil]; + [zipTaskArguments addObject: @"attachments.zip"]; + + // Fetch attachments and write them on disk + attachments = [self fetchFileAttachments]; + max = [attachments count]; + for (count = 0; count < max; count++) + { + currentAttachment = [attachments objectAtIndex: count]; + body = [currentAttachment objectForKey: @"body"]; + name = [[currentAttachment objectForKey: @"filename"] asSafeFilename]; + + fileName = [NSString stringWithFormat:@"%@/%@", spoolPath, name]; + [body writeToFile: fileName atomically: YES]; + + [zipTaskArguments addObject: name]; + } + + // Zip files + [zipTask setArguments: zipTaskArguments]; + [zipTask launch]; + [zipTask waitUntilExit]; + [zipTask release]; + zipContent = [[NSData alloc] initWithContentsOfFile: + [NSString stringWithFormat: @"%@/attachments.zip", spoolPath]]; + + // Delete attachments from disk + max = [zipTaskArguments count]; + for (count = 0; count < max; count++) + { + fileName = [zipTaskArguments objectAtIndex: count]; + [fm removeFileAtPath: + [NSString stringWithFormat: @"%@/%@", spoolPath, fileName] handler: nil]; + } + + // Prepare response + response = [context response]; + baseName = [archiveName stringByDeletingPathExtension]; + extension = [archiveName pathExtension]; + if ([extension length] > 0) + extension = [@"." stringByAppendingString: extension]; + else + extension = @""; + + qpFileName = [NSString stringWithFormat: @"%@%@", + [baseName asQPSubjectString: @"utf-8"], extension]; + [response setHeader: [NSString stringWithFormat: @"application/zip;" + @" name=\"%@\"", qpFileName] + forKey: @"content-type"]; + [response setHeader: [NSString stringWithFormat: @"attachment; filename=\"%@\"", + qpFileName] + forKey: @"Content-Disposition"]; + [response setContent: zipContent]; + + [zipContent release]; + + return response; +} + /* convert parts to strings */ - (NSString *) stringForData: (NSData *) _data partInfo: (NSDictionary *) _info { NSString *charset, *s; NSData *mailData; - + if ([_data isNotNull]) { mailData @@ -934,13 +1092,13 @@ static BOOL debugSoParts = NO; { s = [NSString stringWithData: mailData usingEncodingNamed: charset]; } - + // 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]; } @@ -975,17 +1133,17 @@ static BOOL debugSoParts = NO; /* The fetched parts are NSData objects, this method converts them into NSString objects based on the information inside the bodystructure. - + The fetch-keys are body fetch-keys like: body[text] or body[1.2.3]. The keys in the result dictionary are "" for 'text' and 1.2.3 for parts. */ NSDictionary *datas; - + if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil) return nil; if ([datas isKindOfClass:[NSException class]]) return datas; - + return [self stringifyTextParts:datas]; } @@ -1065,13 +1223,13 @@ static BOOL debugSoParts = NO; acquire: (BOOL) _flag { id obj; - + /* first check attributes directly bound to the application */ if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil) return obj; - + /* lookup body part */ - + if ([self isBodyPartKey:_key]) { if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) { if (debugSoParts) @@ -1087,7 +1245,7 @@ static BOOL debugSoParts = NO; [obj setAsAttachment]; return obj; } - + /* return 404 to stop acquisition */ return [NSException exceptionWithHTTPStatus:404 /* Not Found */ reason: @"Did not find mail method or part-reference!"]; @@ -1131,7 +1289,7 @@ static BOOL debugSoParts = NO; newName: (NSString *) _name inContext: (id)_ctx { - /* + /* Note: this is special because we create SOGoMailObject's even if they do not exist (for performance reasons). @@ -1181,7 +1339,7 @@ static BOOL debugSoParts = NO; NSException *error; WOResponse *r; NSData *content; - + if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) { /* check whether the mail still exists */ if (![self doesMailExist]) { @@ -1190,7 +1348,7 @@ static BOOL debugSoParts = NO; } return error; /* return 304 or 416 */ } - + content = [self content]; if ([content isKindOfClass:[NSException class]]) return content; @@ -1198,7 +1356,7 @@ static BOOL debugSoParts = NO; return [NSException exceptionWithHTTPStatus:404 /* Not Found */ reason: @"did not find IMAP4 message"]; } - + r = [(WOContext *)_ctx response]; [r setHeader: @"message/rfc822" forKey: @"content-type"]; [r setContent:content]; @@ -1257,19 +1415,19 @@ static BOOL debugSoParts = NO; inContext: _ctx]) { /* b) mark deleted */ - + error = [[self imap4Connection] markURLDeleted: [self imap4URL]]; if (error != nil) return error; [self flushMailCaches]; } - + return nil; } - (NSException *) delete { - /* + /* Note: delete is different to DELETEAction: for mails! The 'delete' runs either flags a message as deleted or moves it to the Trash while the DELETEAction: really deletes a message (by flagging it as @@ -1279,7 +1437,7 @@ static BOOL debugSoParts = NO; NSException *error; // TODO: check for safe HTTP method - + error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; return error; } @@ -1287,15 +1445,15 @@ static BOOL debugSoParts = NO; - (id) DELETEAction: (id) _ctx { NSException *error; - + // TODO: ensure safe HTTP method - + error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; if (error != nil) return error; - + error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]]; if (error != nil) return error; // TODO: unflag as deleted? - + return [NSNumber numberWithBool:YES]; /* delete was successful */ } @@ -1304,20 +1462,20 @@ static BOOL debugSoParts = NO; - (BOOL) isMailingListMail { NSDictionary *h; - + if ((h = [self mailHeaders]) == nil) return NO; - + return [[h objectForKey: @"list-id"] isNotEmpty]; } - (BOOL) isVirusScanned { NSDictionary *h; - + if ((h = [self mailHeaders]) == nil) return NO; - + if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO; if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO; return YES; @@ -1329,10 +1487,10 @@ static BOOL debugSoParts = NO; /* Note: not very tolerant on embedded commands and <> */ // TODO: does not really belong here, should be a header-field-parser NSRange r; - + if (![_value isNotEmpty]) return nil; - + if ([_value isKindOfClass:[NSArray class]]) { NSEnumerator *e; id value; @@ -1344,10 +1502,10 @@ static BOOL debugSoParts = NO; } return nil; } - + if (![_value isKindOfClass:[NSString class]]) return nil; - + /* check for commas in string values */ r = [_value rangeOfString: @","]; if (r.length > 0) { @@ -1358,7 +1516,7 @@ static BOOL debugSoParts = NO; /* value qualifies */ if (![(NSString *)_value hasPrefix:_prefix]) return nil; - + /* unquote */ if ([_value characterAtIndex:0] == '<') { r = [_value rangeOfString: @">"]; @@ -1456,7 +1614,7 @@ static BOOL debugSoParts = NO; if (property) { parts = [NSArray arrayWithObject: property]; - + msgs = [self fetchParts: parts]; msgs = [msgs valueForKey: @"fetch"]; if ([msgs count]) { @@ -1512,7 +1670,7 @@ static BOOL debugSoParts = NO; // date already exists, but this one is the correct format - (NSString *) davDate { - return [[self date] rfc822DateString]; + return [[self date] rfc822DateString]; } - (BOOL) hasAttachment @@ -1597,12 +1755,12 @@ static BOOL debugSoParts = NO; if ([fetch count]) { data = [fetch objectForKey: @"header"]; - value = [[NSString alloc] initWithData: data + value = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; range = [value rangeOfString: @"received:" options: NSCaseInsensitiveSearch range: NSMakeRange (10, [value length] - 11)]; - if (range.length + if (range.length && range.location < [value length] && range.length < [value length]) { diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index b0cc629a4..a894f3b65 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -217,6 +217,12 @@ /* Message view "more" menu: create a task from message */ "Convert To Task" = "Convert To Task"; +/* Message view "more" menu: download all attachments as a zip archive */ +"Download all attachments" = "Download all attachments"; + +/* Filename prefix when downloading all attachments as a zip archive */ +"attachments" = "attachments"; + "Print..." = "Print..."; "Delete Message" = "Delete Message"; "Delete Selected Messages" = "Delete Selected Messages"; diff --git a/UI/MailerUI/UIxMailView.m b/UI/MailerUI/UIxMailView.m index 02888400d..ceef38669 100644 --- a/UI/MailerUI/UIxMailView.m +++ b/UI/MailerUI/UIxMailView.m @@ -312,6 +312,18 @@ static NSString *mailETag = nil; return response; } +- (id ) archiveAttachmentsAction +{ + NSString *name; + SOGoMailObject *co; + + co = [self clientObject]; + name = [NSString stringWithFormat: @"%@-%@.zip", + [self labelForKey: @"attachments"], [co nameInContainer]]; + + return [co archiveAllFilesinArchiveNamed: name]; +} + /* MDN */ - (BOOL) _userHasEMail: (NSString *) email diff --git a/UI/MailerUI/product.plist b/UI/MailerUI/product.plist index 930ea1eca..f4c2dac35 100644 --- a/UI/MailerUI/product.plist +++ b/UI/MailerUI/product.plist @@ -244,6 +244,11 @@ pageName = "UIxMailView"; actionName = "sendMDN"; }; + archiveAttachments = { + protectedBy = "View"; + pageName = "UIxMailView"; + actionName = "archiveAttachments"; + }; viewsource = { protectedBy = "View"; actionClass = "UIxMailSourceView"; diff --git a/UI/Templates/MailerUI/UIxMailViewTemplate.wox b/UI/Templates/MailerUI/UIxMailViewTemplate.wox index 8d1c02ebe..9a96fa42d 100644 --- a/UI/Templates/MailerUI/UIxMailViewTemplate.wox +++ b/UI/Templates/MailerUI/UIxMailViewTemplate.wox @@ -84,10 +84,16 @@ + ng-click="viewer.message.download()"> + + + + + diff --git a/UI/WebServerResources/js/Common/Resource.service.js b/UI/WebServerResources/js/Common/Resource.service.js index 83e199ef2..b80f5b4a7 100644 --- a/UI/WebServerResources/js/Common/Resource.service.js +++ b/UI/WebServerResources/js/Common/Resource.service.js @@ -192,14 +192,21 @@ }, responseType: 'arraybuffer', cache: false, - transformResponse: function (data, headers) { + transformResponse: function (data, headers, status) { var fileName, result, blob = null; + if (status < 200 || status > 299) { + throw new Error('Bad gateway'); + } if (data) { blob = new Blob([data], { type: type }); } - fileName = getFileNameFromHeader(headers('content-disposition')); - + if (options && options.filename) { + fileName = options.filename; + } + else { + getFileNameFromHeader(headers('content-disposition')); + } if (!saveAs) { throw new Error('To use Resource.download, FileSaver.js must be loaded.'); } diff --git a/UI/WebServerResources/js/Contacts/AddressBook.service.js b/UI/WebServerResources/js/Contacts/AddressBook.service.js index e59670bba..ce6b0fd03 100644 --- a/UI/WebServerResources/js/Contacts/AddressBook.service.js +++ b/UI/WebServerResources/js/Contacts/AddressBook.service.js @@ -659,7 +659,7 @@ AddressBook.prototype.$deleteCards = function(cards) { var _this = this, ids = _.map(cards, 'id'); - + return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: ids}).then(function() { _this.$_deleteCards(ids); }); @@ -713,14 +713,19 @@ * @returns a promise of the HTTP operation */ AddressBook.prototype.exportCards = function(selectedOnly) { - var selectedUIDs; + var data = null, options, selectedCards; + + options = { + type: 'application/octet-stream', + filename: this.name + '.ldif' + }; if (selectedOnly) { - var selectedCards = _.filter(this.$cards, function(card) { return card.selected; }); - selectedUIDs = _.map(selectedCards, 'id'); + selectedCards = _.filter(this.$cards, function(card) { return card.selected; }); + data = { uids: _.map(selectedCards, 'id') }; } - return AddressBook.$$resource.download(this.id, 'export', (angular.isDefined(selectedUIDs) ? {uids: selectedUIDs} : null), {type: 'application/octet-stream'}); + return AddressBook.$$resource.download(this.id, 'export', data, options); }; /** diff --git a/UI/WebServerResources/js/Contacts/Card.service.js b/UI/WebServerResources/js/Contacts/Card.service.js index d339b1a55..f08b462dd 100644 --- a/UI/WebServerResources/js/Contacts/Card.service.js +++ b/UI/WebServerResources/js/Contacts/Card.service.js @@ -269,11 +269,15 @@ * @returns a promise of the HTTP operation */ Card.prototype.export = function() { - var selectedIDs; + var data, options; - selectedIDs = [ this.id ]; + data = { uids: [ this.id ] }; + options = { + type: 'application/octet-stream', + filename: this.$$fullname + '.ldif' + }; - return Card.$$resource.download(this.pid, 'export', {uids: selectedIDs}, {type: 'application/octet-stream'}); + return Card.$$resource.download(this.pid, 'export', data, options); }; Card.prototype.$fullname = function(options) { diff --git a/UI/WebServerResources/js/Mailer/Mailbox.service.js b/UI/WebServerResources/js/Mailer/Mailbox.service.js index 60db13e66..4ec5e5702 100644 --- a/UI/WebServerResources/js/Mailer/Mailbox.service.js +++ b/UI/WebServerResources/js/Mailer/Mailbox.service.js @@ -598,10 +598,12 @@ * @returns a promise of the HTTP operation */ Mailbox.prototype.saveSelectedMessages = function() { - var selectedMessages, selectedUIDs; + var data, options, selectedMessages, selectedUIDs; selectedMessages = _.filter(this.$messages, function(message) { return message.selected; }); selectedUIDs = _.map(selectedMessages, 'uid'); + data = { uids: selectedUIDs }; + options = { filename: l('Saved Messages.zip') }; return Mailbox.$$resource.download(this.id, 'saveMessages', {uids: selectedUIDs}); }; @@ -613,7 +615,11 @@ * @returns a promise of the HTTP operation */ Mailbox.prototype.exportFolder = function() { - return Mailbox.$$resource.download(this.id, 'exportFolder'); + var options; + + options = { filename: this.name + '.zip' }; + + return Mailbox.$$resource.download(this.id, 'exportFolder', null, options); }; /** @@ -742,7 +748,7 @@ return _this.$_deleteMessages(uids, messages); }); }; - + /** * @function $reset * @memberof Mailbox.prototype diff --git a/UI/WebServerResources/js/Mailer/Message.service.js b/UI/WebServerResources/js/Mailer/Message.service.js index 314e1a45d..9344784d1 100644 --- a/UI/WebServerResources/js/Mailer/Message.service.js +++ b/UI/WebServerResources/js/Mailer/Message.service.js @@ -444,9 +444,9 @@ * @function $imipAction * @memberof Message.prototype * @desc Perform IMIP actions on the current message. - * @param {string} path - the path of the IMIP calendar part + * @param {string} path - the path of the IMIP calendar part * @param {string} action - the the IMIP action to perform - * @param {object} data - the delegation info + * @param {object} data - the delegation info */ Message.prototype.$imipAction = function(path, action, data) { var _this = this; @@ -657,7 +657,7 @@ /** * @function $unwrap * @memberof Message.prototype - * @desc Unwrap a promise. + * @desc Unwrap a promise. * @param {promise} futureMessageData - a promise of some of the Message's data */ Message.prototype.$unwrap = function(futureMessageData) { @@ -708,17 +708,32 @@ }; /** - * @function saveMessage + * @function download * @memberof Message.prototype * @desc Download the current message * @returns a promise of the HTTP operation */ - Message.prototype.saveMessage = function() { - var selectedUIDs; + Message.prototype.download = function() { + var data, options; - selectedUIDs = [ this.uid ]; + data = { uids: [this.uid] }; + options = { filename: this.subject + '.zip' }; - return Message.$$resource.download(this.$mailbox.id, 'saveMessages', {uids: selectedUIDs}); + return Message.$$resource.download(this.$mailbox.id, 'saveMessages', data, options); + }; + + /** + * @function downloadAttachments + * @memberof Message.prototype + * @desc Download an archive of all attachments + * @returns a promise of the HTTP operation + */ + Message.prototype.downloadAttachments = function() { + var options; + + options = { filename: l('attachments') + "-" + this.uid + ".zip" }; + + return Message.$$resource.download(this.$absolutePath(), 'archiveAttachments', null, options); }; })(); diff --git a/UI/WebServerResources/js/Scheduler/Calendar.service.js b/UI/WebServerResources/js/Scheduler/Calendar.service.js index 697993a1e..10e8912ab 100644 --- a/UI/WebServerResources/js/Scheduler/Calendar.service.js +++ b/UI/WebServerResources/js/Scheduler/Calendar.service.js @@ -530,7 +530,14 @@ * @returns a promise of the HTTP operation */ Calendar.prototype.export = function() { - return Calendar.$$resource.download(this.id + '.ics', 'export', null, {type: 'application/octet-stream'}); + var options; + + options = { + type: 'application/octet-stream', + filename: this.name + '.ics' + }; + + return Calendar.$$resource.download(this.id + '.ics', 'export', null, options); }; /**