Download attachments of a message as a zip archive

This commit is contained in:
Francis Lachapelle 2016-12-07 14:37:59 -05:00
parent 2a2ebd553e
commit 011fae8a65
17 changed files with 487 additions and 289 deletions

3
NEWS
View file

@ -1,6 +1,9 @@
3.2.5 (2016-12-DD) 3.2.5 (2016-12-DD)
------------------ ------------------
New features
- [web] download attachments of a message as a zip archive
Enhancements Enhancements
- [web] prevent using localhost on additional IMAP accounts - [web] prevent using localhost on additional IMAP accounts

View file

@ -33,6 +33,7 @@
fromIndex: (int) start; fromIndex: (int) start;
- (int) indexOf: (unichar) _c; - (int) indexOf: (unichar) _c;
- (NSString *) decodedHeader; - (NSString *) decodedHeader;
- (NSString *) asSafeFilename;
@end @end

View file

@ -688,4 +688,26 @@ convertChars (const char *oldString, unsigned int oldLength,
return decodedHeader; 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 @end

View file

@ -97,7 +97,6 @@
/* attachments */ /* attachments */
- (NSArray *) fetchAttachmentAttrs; - (NSArray *) fetchAttachmentAttrs;
- (BOOL) isValidAttachmentName: (NSString *) _name;
- (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name; - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name;
- (NSString *) pathToAttachmentWithName: (NSString *) _name; - (NSString *) pathToAttachmentWithName: (NSString *) _name;
- (NSException *) saveAttachment: (NSData *) _attach - (NSException *) saveAttachment: (NSData *) _attach

View file

@ -845,59 +845,22 @@ static NSString *userAgent = nil;
} }
} }
//
//
//
- (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 - (void) _fetchAttachmentsFromMail: (SOGoMailObject *) sourceMail
{ {
unsigned int count, max; unsigned int max, count;
NSArray *parts, *paths, *bodies; NSArray *attachments;
NSData *body;
NSDictionary *currentInfo; NSDictionary *currentInfo;
NGHashMap *response;
parts = [sourceMail fetchFileAttachmentKeys]; attachments = [sourceMail fetchFileAttachments];
max = [parts count]; max = [attachments count];
if (max > 0) for (count = 0; count < max; count++)
{ {
paths = [parts keysWithFormat: @"BODY[%{path}]"]; currentInfo = [attachments objectAtIndex: count];
response = [[sourceMail fetchParts: paths] objectForKey: @"RawResponse"]; [self saveAttachment: [currentInfo objectForKey: @"body"]
bodies = [self _attachmentBodiesFromPaths: paths withMetadata: currentInfo];
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];
}
} }
} }
@ -1098,12 +1061,6 @@ static NSString *userAgent = nil;
return ma; return ma;
} }
- (BOOL) isValidAttachmentName: (NSString *) filename
{
return (!([filename rangeOfString: @"/"].length
|| [filename isEqualToString: @"."]
|| [filename isEqualToString: @".."]));
}
- (NSString *) pathToAttachmentWithName: (NSString *) _name - (NSString *) pathToAttachmentWithName: (NSString *) _name
{ {
@ -1113,17 +1070,15 @@ static NSString *userAgent = nil;
return [[self draftFolderPath] stringByAppendingPathComponent:_name]; 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 - (NSException *) saveAttachment: (NSData *) _attach
withMetadata: (NSDictionary *) metadata withMetadata: (NSDictionary *) metadata
{ {
NSString *p, *pmime, *name, *mimeType; NSString *p, *pmime, *name, *mimeType;
NSRange r;
if (![_attach isNotNull]) { if (![_attach isNotNull]) {
return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
@ -1135,15 +1090,7 @@ static NSString *userAgent = nil;
reason: @"Could not create folder for draft!"]; reason: @"Could not create folder for draft!"];
} }
name = [metadata objectForKey: @"filename"]; name = [[metadata objectForKey: @"filename"] asSafeFilename];
r = [name rangeOfString: @"\\"
options: NSBackwardsSearch];
if (r.length > 0)
name = [name substringFromIndex: r.location + 1];
if (![self isValidAttachmentName: name])
return [self invalidAttachmentNameError: name];
p = [self pathToAttachmentWithName: name]; p = [self pathToAttachmentWithName: name];
if (![_attach writeToFile: p atomically: YES]) if (![_attach writeToFile: p atomically: YES])
{ {
@ -1173,19 +1120,12 @@ static NSString *userAgent = nil;
NSException *error; NSException *error;
error = nil; error = nil;
fm = [NSFileManager defaultManager];
if ([self isValidAttachmentName:_name]) p = [self pathToAttachmentWithName: [_name asSafeFilename]];
{ if ([fm fileExistsAtPath: p])
fm = [NSFileManager defaultManager]; if (![fm removeFileAtPath: p handler: nil])
p = [self pathToAttachmentWithName:_name]; error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
if ([fm fileExistsAtPath: p]) reason: @"Could not delete attachment from draft!"];
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;
} }
@ -1288,8 +1228,7 @@ static NSString *userAgent = nil;
NSString *s, *p; NSString *s, *p;
NSData *mimeData; NSData *mimeData;
p = [self pathToAttachmentWithName: p = [self pathToAttachmentWithName: [NSString stringWithFormat: @".%@.mime", _name]];
[NSString stringWithFormat: @".%@.mime", _name]];
mimeData = [NSData dataWithContentsOfFile: p]; mimeData = [NSData dataWithContentsOfFile: p];
if (mimeData) if (mimeData)
{ {
@ -1308,20 +1247,18 @@ static NSString *userAgent = nil;
} }
- (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
andContentType: (NSString *) _type
{ {
NSString *type;
NSString *cdtype; NSString *cdtype;
NSString *cd; NSString *cd;
SOGoDomainDefaults *dd; SOGoDomainDefaults *dd;
type = [self contentTypeForAttachmentWithName:_name]; if ([_type hasPrefix: @"text/"])
if ([type hasPrefix: @"text/"])
{ {
dd = [[context activeUser] domainDefaults]; dd = [[context activeUser] domainDefaults];
cdtype = [dd mailAttachTextDocumentsInline] ? @"inline" : @"attachment"; cdtype = [dd mailAttachTextDocumentsInline] ? @"inline" : @"attachment";
} }
else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"]) else if ([_type hasPrefix: @"image/"] || [_type hasPrefix: @"message"])
cdtype = @"inline"; cdtype = @"inline";
else else
cdtype = @"attachment"; cdtype = @"attachment";
@ -1369,7 +1306,7 @@ static NSString *userAgent = nil;
else if ([s hasPrefix: @"message/rfc822"]) else if ([s hasPrefix: @"message/rfc822"])
attachAsRFC822 = YES; attachAsRFC822 = YES;
} }
if ((s = [self contentDispositionForAttachmentWithName: _name])) if ((s = [self contentDispositionForAttachmentWithName: _name andContentType: s]))
{ {
NGMimeContentDispositionHeaderField *o; NGMimeContentDispositionHeaderField *o;

View file

@ -1,4 +1,5 @@
/* /*
Copyright (C) 2009-2016 Inverse inc.
Copyright (C) 2004-2005 SKYRIX Software AG Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of OpenGroupware.org. This file is part of OpenGroupware.org.
@ -48,6 +49,8 @@
@class NGImap4Envelope; @class NGImap4Envelope;
@class NGImap4EnvelopeAddress; @class NGImap4EnvelopeAddress;
@class WOResponse;
NSArray *SOGoMailCoreInfoKeys; NSArray *SOGoMailCoreInfoKeys;
@interface SOGoMailObject : SOGoMailBaseObject @interface SOGoMailObject : SOGoMailBaseObject
@ -107,6 +110,8 @@ NSArray *SOGoMailCoreInfoKeys;
- (BOOL) hasAttachment; - (BOOL) hasAttachment;
- (NSDictionary *) fetchFileAttachmentIds; - (NSDictionary *) fetchFileAttachmentIds;
- (NSArray *) fetchFileAttachmentKeys; - (NSArray *) fetchFileAttachmentKeys;
- (NSArray *) fetchFileAttachments;
- (WOResponse *) archiveAllFilesinArchiveNamed: (NSString *) archiveName;
/* flags */ /* flags */

View file

@ -1,5 +1,5 @@
/* /*
Copyright (C) 2007-2014 Inverse inc. Copyright (C) 2007-2016 Inverse inc.
Copyright (C) 2004-2005 SKYRIX Software AG Copyright (C) 2004-2005 SKYRIX Software AG
This file is part of SOGo. This file is part of SOGo.
@ -20,6 +20,7 @@
02111-1307, USA. 02111-1307, USA.
*/ */
#import <Foundation/NSTask.h>
#import <Foundation/NSURL.h> #import <Foundation/NSURL.h>
#import <Foundation/NSValue.h> #import <Foundation/NSValue.h>
@ -27,6 +28,7 @@
#import <NGObjWeb/WORequest.h> #import <NGObjWeb/WORequest.h>
#import <NGObjWeb/NSException+HTTP.h> #import <NGObjWeb/NSException+HTTP.h>
#import <NGExtensions/NGHashMap.h> #import <NGExtensions/NGHashMap.h>
#import <NGExtensions/NSFileManager+Extensions.h>
#import <NGExtensions/NSNull+misc.h> #import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h> #import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+Encoding.h> #import <NGExtensions/NSString+Encoding.h>
@ -38,7 +40,9 @@
#import <SOGo/NSArray+Utilities.h> #import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h> #import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoPermissions.h> #import <SOGo/SOGoPermissions.h>
#import <SOGo/SOGoSystemDefaults.h>
#import <SOGo/SOGoUser.h> #import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserDefaults.h> #import <SOGo/SOGoUserDefaults.h>
#import <SOGo/NSCalendarDate+SOGo.h> #import <SOGo/NSCalendarDate+SOGo.h>
@ -913,6 +917,160 @@ static BOOL debugSoParts = NO;
return keys; 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 */ /* convert parts to strings */
- (NSString *) stringForData: (NSData *) _data - (NSString *) stringForData: (NSData *) _data
partInfo: (NSDictionary *) _info partInfo: (NSDictionary *) _info

View file

@ -217,6 +217,12 @@
/* Message view "more" menu: create a task from message */ /* Message view "more" menu: create a task from message */
"Convert To Task" = "Convert To Task"; "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..."; "Print..." = "Print...";
"Delete Message" = "Delete Message"; "Delete Message" = "Delete Message";
"Delete Selected Messages" = "Delete Selected Messages"; "Delete Selected Messages" = "Delete Selected Messages";

View file

@ -312,6 +312,18 @@ static NSString *mailETag = nil;
return response; return response;
} }
- (id <WOActionResults>) archiveAttachmentsAction
{
NSString *name;
SOGoMailObject *co;
co = [self clientObject];
name = [NSString stringWithFormat: @"%@-%@.zip",
[self labelForKey: @"attachments"], [co nameInContainer]];
return [co archiveAllFilesinArchiveNamed: name];
}
/* MDN */ /* MDN */
- (BOOL) _userHasEMail: (NSString *) email - (BOOL) _userHasEMail: (NSString *) email

View file

@ -244,6 +244,11 @@
pageName = "UIxMailView"; pageName = "UIxMailView";
actionName = "sendMDN"; actionName = "sendMDN";
}; };
archiveAttachments = {
protectedBy = "View";
pageName = "UIxMailView";
actionName = "archiveAttachments";
};
viewsource = { viewsource = {
protectedBy = "View"; protectedBy = "View";
actionClass = "UIxMailSourceView"; actionClass = "UIxMailSourceView";

View file

@ -84,10 +84,16 @@
</md-menu-item> </md-menu-item>
<md-menu-item> <md-menu-item>
<md-button label:aria-label="Save As..." <md-button label:aria-label="Save As..."
ng-click="viewer.message.saveMessage()"> ng-click="viewer.message.download()">
<var:string label:value="Save As..."/> <var:string label:value="Save As..."/>
</md-button> </md-button>
</md-menu-item> </md-menu-item>
<md-menu-item ng-show="::viewer.message.attachmentAttrs.length">
<md-button label:aria-label="Download all attachments"
ng-click="viewer.message.downloadAttachments()">
<var:string label:value="Download all attachments"/>
</md-button>
</md-menu-item>
<md-menu-item> <md-menu-item>
<md-button label:aria-label="View Message Source" <md-button label:aria-label="View Message Source"
ng-click="viewer.toggleRawSource($event)"> ng-click="viewer.toggleRawSource($event)">

View file

@ -192,14 +192,21 @@
}, },
responseType: 'arraybuffer', responseType: 'arraybuffer',
cache: false, cache: false,
transformResponse: function (data, headers) { transformResponse: function (data, headers, status) {
var fileName, result, blob = null; var fileName, result, blob = null;
if (status < 200 || status > 299) {
throw new Error('Bad gateway');
}
if (data) { if (data) {
blob = new Blob([data], { type: type }); 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) { if (!saveAs) {
throw new Error('To use Resource.download, FileSaver.js must be loaded.'); throw new Error('To use Resource.download, FileSaver.js must be loaded.');
} }

View file

@ -713,14 +713,19 @@
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
AddressBook.prototype.exportCards = function(selectedOnly) { AddressBook.prototype.exportCards = function(selectedOnly) {
var selectedUIDs; var data = null, options, selectedCards;
options = {
type: 'application/octet-stream',
filename: this.name + '.ldif'
};
if (selectedOnly) { if (selectedOnly) {
var selectedCards = _.filter(this.$cards, function(card) { return card.selected; }); selectedCards = _.filter(this.$cards, function(card) { return card.selected; });
selectedUIDs = _.map(selectedCards, 'id'); 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);
}; };
/** /**

View file

@ -269,11 +269,15 @@
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Card.prototype.export = function() { 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) { Card.prototype.$fullname = function(options) {

View file

@ -598,10 +598,12 @@
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Mailbox.prototype.saveSelectedMessages = function() { Mailbox.prototype.saveSelectedMessages = function() {
var selectedMessages, selectedUIDs; var data, options, selectedMessages, selectedUIDs;
selectedMessages = _.filter(this.$messages, function(message) { return message.selected; }); selectedMessages = _.filter(this.$messages, function(message) { return message.selected; });
selectedUIDs = _.map(selectedMessages, 'uid'); selectedUIDs = _.map(selectedMessages, 'uid');
data = { uids: selectedUIDs };
options = { filename: l('Saved Messages.zip') };
return Mailbox.$$resource.download(this.id, 'saveMessages', {uids: selectedUIDs}); return Mailbox.$$resource.download(this.id, 'saveMessages', {uids: selectedUIDs});
}; };
@ -613,7 +615,11 @@
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Mailbox.prototype.exportFolder = function() { 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);
}; };
/** /**

View file

@ -708,17 +708,32 @@
}; };
/** /**
* @function saveMessage * @function download
* @memberof Message.prototype * @memberof Message.prototype
* @desc Download the current message * @desc Download the current message
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Message.prototype.saveMessage = function() { Message.prototype.download = function() {
var selectedUIDs; 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);
}; };
})(); })();

View file

@ -530,7 +530,14 @@
* @returns a promise of the HTTP operation * @returns a promise of the HTTP operation
*/ */
Calendar.prototype.export = function() { 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);
}; };
/** /**