Download attachments of a message as a zip archive

pull/233/head
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

@ -48,11 +48,11 @@
@interface _SOGoHTMLContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler> @interface _SOGoHTMLContentHandler : NSObject <SaxContentHandler, SaxLexicalHandler>
{ {
NSMutableArray *images; NSMutableArray *images;
NSArray *ignoreContentTags; NSArray *ignoreContentTags;
NSArray *specialTreatmentTags; NSArray *specialTreatmentTags;
NSArray *voidTags; NSArray *voidTags;
BOOL ignoreContent; BOOL ignoreContent;
BOOL orderedList; BOOL orderedList;
BOOL unorderedList; BOOL unorderedList;
@ -300,7 +300,7 @@
i = [value indexOf: ';']; i = [value indexOf: ';'];
j = [value indexOf: ';' fromIndex: i+1]; j = [value indexOf: ';' fromIndex: i+1];
k = [value indexOf: ',']; k = [value indexOf: ','];
// We try to get the MIME type // We try to get the MIME type
mimeType = nil; mimeType = nil;
@ -314,7 +314,7 @@
// We might get a stupid value. We discard anything that doesn't have a / in it // We might get a stupid value. We discard anything that doesn't have a / in it
if ([mimeType indexOf: '/'] < 0) if ([mimeType indexOf: '/'] < 0)
mimeType = @"image/jpeg"; mimeType = @"image/jpeg";
// We check and skip the charset // We check and skip the charset
if (j < i) if (j < i)
j = i; j = i;
@ -335,14 +335,14 @@
[map setObject: [NSString stringWithFormat: @"inline; filename=\"%@\"", uniqueId] forKey: @"content-disposition"]; [map setObject: [NSString stringWithFormat: @"inline; filename=\"%@\"", uniqueId] forKey: @"content-disposition"];
[map setObject: [NSString stringWithFormat: @"%@; name=\"%@\"", mimeType, uniqueId] forKey: @"content-type"]; [map setObject: [NSString stringWithFormat: @"%@; name=\"%@\"", mimeType, uniqueId] forKey: @"content-type"];
[map setObject: [NSString stringWithFormat: @"<%@>", uniqueId] forKey: @"content-id"]; [map setObject: [NSString stringWithFormat: @"<%@>", uniqueId] forKey: @"content-id"];
body = [[NGMimeFileData alloc] initWithBytes: [data bytes] length: [data length]]; body = [[NGMimeFileData alloc] initWithBytes: [data bytes] length: [data length]];
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
[bodyPart setBody: body]; [bodyPart setBody: body];
[body release]; [body release];
[images addObject: bodyPart]; [images addObject: bodyPart];
[result appendFormat: @"<img src=\"cid:%@\" type=\"%@\"", uniqueId, mimeType]; [result appendFormat: @"<img src=\"cid:%@\" type=\"%@\"", uniqueId, mimeType];
@ -357,7 +357,7 @@
[result appendFormat: @" %@=\"%@\"", attrName, value]; [result appendFormat: @" %@=\"%@\"", attrName, value];
} }
} }
[result appendString: @"/>"]; [result appendString: @"/>"];
} }
} }
@ -656,19 +656,19 @@ convertChars (const char *oldString, unsigned int oldLength,
fromIndex: (int) start fromIndex: (int) start
{ {
int i, len; int i, len;
len = [self length]; len = [self length];
if (start < 0 || start >= len) if (start < 0 || start >= len)
start = 0; start = 0;
for (i = start; i < len; i++) for (i = start; i < len; i++)
{ {
if ([self characterAtIndex: i] == _c) return i; if ([self characterAtIndex: i] == _c) return i;
} }
return -1; return -1;
} }
- (int) indexOf: (unichar) _c - (int) indexOf: (unichar) _c
@ -684,8 +684,30 @@ convertChars (const char *oldString, unsigned int oldLength,
decodedHeader]; decodedHeader];
if (!decodedHeader) if (!decodedHeader)
decodedHeader = self; decodedHeader = self;
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

@ -74,7 +74,7 @@
static NSString *contentTypeValue = @"text/plain; charset=utf-8"; static NSString *contentTypeValue = @"text/plain; charset=utf-8";
static NSString *htmlContentTypeValue = @"text/html; 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", @"from", @"replyTo", @"message-id",
nil}; nil};
@ -142,18 +142,18 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
{ {
const char *b, *bytes; const char *b, *bytes;
int i, len, slen; int i, len, slen;
if (!theCString) if (!theCString)
{ {
return NSMakeRange(NSNotFound,0); return NSMakeRange(NSNotFound,0);
} }
bytes = [self bytes]; bytes = [self bytes];
len = [self length]; len = [self length];
slen = strlen(theCString); slen = strlen(theCString);
b = bytes; b = bytes;
if (len > theRange.location + theRange.length) if (len > theRange.location + theRange.length)
{ {
len = theRange.location + theRange.length; len = theRange.location + theRange.length;
@ -163,7 +163,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
{ {
i = theRange.location; i = theRange.location;
b += i; b += i;
for (; i <= len-slen; i++, b++) for (; i <= len-slen; i++, b++)
{ {
if (!strncasecmp(theCString,b,slen)) if (!strncasecmp(theCString,b,slen))
@ -176,7 +176,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
{ {
i = theRange.location; i = theRange.location;
b += i; b += i;
for (; i <= len-slen; i++, b++) for (; i <= len-slen; i++, b++)
{ {
if (!memcmp(theCString,b,slen)) if (!memcmp(theCString,b,slen))
@ -185,7 +185,7 @@ static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
} }
} }
} }
return NSMakeRange(NSNotFound,0); return NSMakeRange(NSNotFound,0);
} }
@ -271,7 +271,7 @@ static NSString *userAgent = nil;
NSFileManager *fm; NSFileManager *fm;
fm = [NSFileManager defaultManager]; fm = [NSFileManager defaultManager];
return ([fm createDirectoriesAtPath: [container userSpoolFolderPath] return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
attributes: nil] attributes: nil]
&& [fm createDirectoriesAtPath: [self draftFolderPath] && [fm createDirectoriesAtPath: [self draftFolderPath]
@ -308,7 +308,7 @@ static NSString *userAgent = nil;
messageID = [NSString generateMessageID]; messageID = [NSString generateMessageID];
[headers setObject: messageID forKey: @"message-id"]; [headers setObject: messageID forKey: @"message-id"];
} }
priority = [newHeaders objectForKey: @"X-Priority"]; priority = [newHeaders objectForKey: @"X-Priority"];
if (priority) if (priority)
{ {
@ -649,7 +649,7 @@ static NSString *userAgent = nil;
atURL: [[self mailAccountFolder] imap4URL]]; atURL: [[self mailAccountFolder] imap4URL]];
[imap4 flushFolderHierarchyCache]; [imap4 flushFolderHierarchyCache];
} }
folder = [imap4 imap4FolderNameForURL: [container imap4URL]]; folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
result = [client append: message toFolder: folder result = [client append: message toFolder: folder
withFlags: [NSArray arrayWithObjects: @"draft", nil]]; 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 there is a 'reply-to' header, only include that (as TO)
- if we reply to all, all non-from addresses are added as CC - if we reply to all, all non-from addresses are added as CC
- the from is always the lone TO (except for reply-to) - 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 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 envelope if none is contained in the message itself! (bug or
feature?) feature?)
@ -776,11 +776,11 @@ static NSString *userAgent = nil;
int i; int i;
identities = [[[self container] mailAccountFolder] identities]; identities = [[[self container] mailAccountFolder] identities];
for (i = 0; i < [identities count]; i++) for (i = 0; i < [identities count]; i++)
{ {
email = [[identities objectAtIndex: i] objectForKey: @"email"]; email = [[identities objectAtIndex: i] objectForKey: @"email"];
if (email) if (email)
[allRecipients addObject: email]; [allRecipients addObject: email];
} }
@ -838,66 +838,29 @@ static NSString *userAgent = nil;
[self _purgeRecipients: allRecipients [self _purgeRecipients: allRecipients
fromAddresses: addrs]; fromAddresses: addrs];
[self _addEMailsOfAddresses: addrs toArray: to]; [self _addEMailsOfAddresses: addrs toArray: to];
[_info setObject: to forKey: @"cc"]; [_info setObject: to forKey: @"cc"];
[to release]; [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 - (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];
}
} }
} }
@ -1002,14 +965,14 @@ static NSString *userAgent = nil;
SOGoUserDefaults *ud; SOGoUserDefaults *ud;
[sourceMail fetchCoreInfos]; [sourceMail fetchCoreInfos];
if ([sourceMail subjectForForward]) if ([sourceMail subjectForForward])
{ {
info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward] info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
forKey: @"subject"]; forKey: @"subject"];
[self setHeaders: info]; [self setHeaders: info];
} }
[self setSourceURL: [sourceMail imap4URLString]]; [self setSourceURL: [sourceMail imap4URLString]];
[self setSourceFlag: @"$Forwarded"]; [self setSourceFlag: @"$Forwarded"];
[self setSourceIMAP4ID: [[sourceMail nameInContainer] intValue]]; [self setSourceIMAP4ID: [[sourceMail nameInContainer] intValue]];
@ -1052,7 +1015,7 @@ static NSString *userAgent = nil;
- (NSString *) sender - (NSString *) sender
{ {
id tmp; id tmp;
if ((tmp = [headers objectForKey: @"from"]) == nil) if ((tmp = [headers objectForKey: @"from"]) == nil)
return nil; return nil;
if ([tmp isKindOfClass:[NSArray class]]) if ([tmp isKindOfClass:[NSArray class]])
@ -1098,52 +1061,36 @@ 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
{ {
if ([_name length] == 0) if ([_name length] == 0)
return nil; return 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 */
reason: @"Missing attachment content!"]; reason: @"Missing attachment content!"];
} }
if (![self _ensureDraftFolderPath]) { if (![self _ensureDraftFolderPath]) {
return [NSException exceptionWithHTTPStatus:500 /* Server Error */ return [NSException exceptionWithHTTPStatus:500 /* Server Error */
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])
{ {
@ -1162,7 +1109,7 @@ static NSString *userAgent = nil;
reason: @"Could not write attachment to draft!"]; reason: @"Could not write attachment to draft!"];
} }
} }
return nil; /* everything OK */ return nil; /* everything OK */
} }
@ -1173,21 +1120,14 @@ static NSString *userAgent = nil;
NSException *error; NSException *error;
error = nil; 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]) return error;
{
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;
} }
// //
@ -1201,9 +1141,9 @@ static NSString *userAgent = nil;
/* prepare header of body part */ /* prepare header of body part */
map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease]; map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
[map setObject: contentTypeValue forKey: @"content-type"]; [map setObject: contentTypeValue forKey: @"content-type"];
/* prepare body content */ /* prepare body content */
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
@ -1224,7 +1164,7 @@ static NSString *userAgent = nil;
*/ */
NGMutableHashMap *map; NGMutableHashMap *map;
NGMimeBodyPart *bodyPart; NGMimeBodyPart *bodyPart;
/* prepare header of body part */ /* prepare header of body part */
map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease]; map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
@ -1232,7 +1172,7 @@ static NSString *userAgent = nil;
if (text) if (text)
[map setObject: (isHTML ? htmlContentTypeValue : contentTypeValue) [map setObject: (isHTML ? htmlContentTypeValue : contentTypeValue)
forKey: @"content-type"]; forKey: @"content-type"];
/* prepare body content */ /* prepare body content */
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
[bodyPart setBody: text]; [bodyPart setBody: text];
@ -1242,11 +1182,11 @@ static NSString *userAgent = nil;
- (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
{ {
NGMimeMessage *message; NGMimeMessage *message;
id body; id body;
message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease]; message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
if (!isHTML) if (!isHTML)
{ {
[message setHeader: contentTypeValue forKey: @"content-type"]; [message setHeader: contentTypeValue forKey: @"content-type"];
@ -1263,9 +1203,9 @@ static NSString *userAgent = nil;
// Add the HTML part // Add the HTML part
[body addBodyPart: [self bodyPartForText]]; [body addBodyPart: [self bodyPartForText]];
} }
[message setBody: body]; [message setBody: body];
return message; return message;
} }
@ -1287,9 +1227,8 @@ 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";
@ -1348,7 +1285,7 @@ static NSString *userAgent = nil;
if (_name == nil) return nil; if (_name == nil) return nil;
/* check attachment */ /* check attachment */
fm = [NSFileManager defaultManager]; fm = [NSFileManager defaultManager];
p = [self pathToAttachmentWithName: _name]; p = [self pathToAttachmentWithName: _name];
if (![fm isReadableFileAtPath: p]) { if (![fm isReadableFileAtPath: p]) {
@ -1357,7 +1294,7 @@ static NSString *userAgent = nil;
} }
attachAsString = NO; attachAsString = NO;
attachAsRFC822 = NO; attachAsRFC822 = NO;
/* prepare header of body part */ /* prepare header of body part */
map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease]; map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease];
@ -1369,22 +1306,22 @@ 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;
o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s]; o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s];
[map setObject: o forKey: @"content-disposition"]; [map setObject: o forKey: @"content-disposition"];
[o release]; [o release];
} }
/* prepare body content */ /* prepare body content */
if (attachAsString) { // TODO: is this really necessary? if (attachAsString) { // TODO: is this really necessary?
NSString *s; NSString *s;
content = [[NSData alloc] initWithContentsOfMappedFile:p]; content = [[NSData alloc] initWithContentsOfMappedFile:p];
s = [[NSString alloc] initWithData: content s = [[NSString alloc] initWithData: content
encoding: [NSString defaultCStringEncoding]]; encoding: [NSString defaultCStringEncoding]];
if (s != nil) { if (s != nil) {
@ -1399,7 +1336,7 @@ static NSString *userAgent = nil;
} }
} }
else { else {
/* /*
Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
NGMimeFileData objects are not processed by the MIME generator! NGMimeFileData objects are not processed by the MIME generator!
*/ */
@ -1415,17 +1352,17 @@ static NSString *userAgent = nil;
content = [content dataByEncodingBase64]; content = [content dataByEncodingBase64];
[map setObject: @"base64" forKey: @"content-transfer-encoding"]; [map setObject: @"base64" forKey: @"content-transfer-encoding"];
} }
[map setObject:[NSNumber numberWithInt:[content length]] [map setObject:[NSNumber numberWithInt:[content length]]
forKey: @"content-length"]; forKey: @"content-length"];
/* Note: the -init method will create a temporary file! */ /* Note: the -init method will create a temporary file! */
body = [[NGMimeFileData alloc] initWithBytes:[content bytes] body = [[NGMimeFileData alloc] initWithBytes:[content bytes]
length:[content length]]; length:[content length]];
} }
bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
[bodyPart setBody:body]; [bodyPart setBody:body];
[body release]; body = nil; [body release]; body = nil;
return bodyPart; return bodyPart;
} }
@ -1462,14 +1399,14 @@ static NSString *userAgent = nil;
NGMimeMultipartBody *textParts; NGMimeMultipartBody *textParts;
NGMutableHashMap *header; NGMutableHashMap *header;
NGMimeBodyPart *part; NGMimeBodyPart *part;
header = [NGMutableHashMap hashMap]; header = [NGMutableHashMap hashMap];
[header addObject: MultiAlternativeType forKey: @"content-type"]; [header addObject: MultiAlternativeType forKey: @"content-type"];
part = [NGMimeBodyPart bodyPartWithHeader: header]; part = [NGMimeBodyPart bodyPartWithHeader: header];
textParts = [[NGMimeMultipartBody alloc] initWithPart: part]; textParts = [[NGMimeMultipartBody alloc] initWithPart: part];
// Get the text part from it and add it // Get the text part from it and add it
[textParts addBodyPart: [self plainTextBodyPartForText]]; [textParts addBodyPart: [self plainTextBodyPartForText]];
@ -1488,17 +1425,17 @@ static NSString *userAgent = nil;
- (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
andBodyParts: (NSArray *) _bodyParts andBodyParts: (NSArray *) _bodyParts
{ {
NGMimeMessage *message; NGMimeMessage *message;
NGMimeMultipartBody *mBody; NGMimeMultipartBody *mBody;
NSEnumerator *e; NSEnumerator *e;
id part; id part;
[map addObject: MultiMixedType forKey: @"content-type"]; [map addObject: MultiMixedType forKey: @"content-type"];
message = [[NGMimeMessage alloc] initWithHeader: map]; message = [[NGMimeMessage alloc] initWithHeader: map];
[message autorelease]; [message autorelease];
mBody = [[NGMimeMultipartBody alloc] initWithPart: message]; mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
if (!isHTML) if (!isHTML)
{ {
part = [self bodyPartForText]; part = [self bodyPartForText];
@ -1535,11 +1472,11 @@ static NSString *userAgent = nil;
if ([_h count] == 0) if ([_h count] == 0)
return; return;
names = [_h keyEnumerator]; names = [_h keyEnumerator];
while ((name = [names nextObject]) != nil) { while ((name = [names nextObject]) != nil) {
id value; id value;
value = [_h objectForKey:name]; value = [_h objectForKey:name];
[_map addObject:value forKey:name]; [_map addObject:value forKey:name];
} }
@ -1549,10 +1486,10 @@ static NSString *userAgent = nil;
{ {
if (![_value isNotNull]) if (![_value isNotNull])
return YES; return YES;
if ([_value isKindOfClass: [NSArray class]]) if ([_value isKindOfClass: [NSArray class]])
return [_value count] == 0 ? YES : NO; return [_value count] == 0 ? YES : NO;
if ([_value isKindOfClass: [NSString class]]) if ([_value isKindOfClass: [NSString class]])
return [_value length] == 0 ? YES : NO; return [_value length] == 0 ? YES : NO;
@ -1630,9 +1567,9 @@ static NSString *userAgent = nil;
NSString *s, *dateString; NSString *s, *dateString;
NGMutableHashMap *map; NGMutableHashMap *map;
id emails, from, replyTo; id emails, from, replyTo;
map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease]; map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
/* add recipients */ /* add recipients */
if ((emails = [headers objectForKey: @"to"]) != nil && [emails isKindOfClass: [NSArray class]]) if ((emails = [headers objectForKey: @"to"]) != nil && [emails isKindOfClass: [NSArray class]])
[map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"to"]; [map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"to"];
@ -1643,7 +1580,7 @@ static NSString *userAgent = nil;
/* add senders */ /* add senders */
from = [headers objectForKey: @"from"]; from = [headers objectForKey: @"from"];
if (![self isEmptyValue:from]) { if (![self isEmptyValue:from]) {
if ([from isKindOfClass:[NSArray class]]) if ([from isKindOfClass:[NSArray class]])
[map setObjects: [self _quoteSpecialsInArray: from] forKey: @"from"]; [map setObjects: [self _quoteSpecialsInArray: from] forKey: @"from"];
@ -1661,7 +1598,7 @@ static NSString *userAgent = nil;
if ([(s = [headers objectForKey: @"subject"]) length] > 0) if ([(s = [headers objectForKey: @"subject"]) length] > 0)
[map setObject: [s asQPSubjectString: @"utf-8"] [map setObject: [s asQPSubjectString: @"utf-8"]
forKey: @"subject"]; forKey: @"subject"];
if ([(s = [headers objectForKey: @"message-id"]) length] > 0) if ([(s = [headers objectForKey: @"message-id"]) length] > 0)
[map setObject: s [map setObject: s
forKey: @"message-id"]; forKey: @"message-id"];
@ -1685,16 +1622,16 @@ static NSString *userAgent = nil;
forKey: @"Disposition-Notification-To"]; forKey: @"Disposition-Notification-To"];
[self _addHeaders: _headers toHeaderMap: map]; [self _addHeaders: _headers toHeaderMap: map];
// We remove what we have to... // We remove what we have to...
if (_exclude) if (_exclude)
{ {
int i; int i;
for (i = 0; i < [_exclude count]; i++) for (i = 0; i < [_exclude count]; i++)
[map removeAllObjectsForKey: [_exclude objectAtIndex: i]]; [map removeAllObjectsForKey: [_exclude objectAtIndex: i]];
} }
return map; return map;
} }
@ -1730,11 +1667,11 @@ static NSString *userAgent = nil;
if (map) if (map)
{ {
//[self debugWithFormat: @"MIME Envelope: %@", map]; //[self debugWithFormat: @"MIME Envelope: %@", map];
[bodyParts addObjectsFromArray: [self bodyPartsForAllAttachments]]; [bodyParts addObjectsFromArray: [self bodyPartsForAllAttachments]];
//[self debugWithFormat: @"attachments: %@", bodyParts]; //[self debugWithFormat: @"attachments: %@", bodyParts];
if ([bodyParts count] == 0) if ([bodyParts count] == 0)
/* no attachments */ /* no attachments */
message = [self mimeMessageForContentWithHeaderMap: map]; message = [self mimeMessageForContentWithHeaderMap: map];
@ -1748,13 +1685,13 @@ static NSString *userAgent = nil;
[map addObject: MultiRelatedType forKey: @"content-type"]; [map addObject: MultiRelatedType forKey: @"content-type"];
} }
message = [self mimeMultiPartMessageWithHeaderMap: map message = [self mimeMultiPartMessageWithHeaderMap: map
andBodyParts: bodyParts]; andBodyParts: bodyParts];
//[self debugWithFormat: @"message: %@", message]; //[self debugWithFormat: @"message: %@", message];
} }
} }
return message; return message;
} }
@ -1827,9 +1764,9 @@ static NSString *userAgent = nil;
- (NSException *) sendMail - (NSException *) sendMail
{ {
SOGoUserDefaults *ud; SOGoUserDefaults *ud;
ud = [[context activeUser] userDefaults]; ud = [[context activeUser] userDefaults];
if ([ud mailAddOutgoingAddresses]) if ([ud mailAddOutgoingAddresses])
{ {
NSString *recipient, *emailAddress, *addressBook, *uid; NSString *recipient, *emailAddress, *addressBook, *uid;
@ -1840,9 +1777,9 @@ static NSString *userAgent = nil;
NGMailAddressParser *parser; NGMailAddressParser *parser;
SOGoFolder <SOGoContactFolder> *folder; SOGoFolder <SOGoContactFolder> *folder;
NGVCard *card; NGVCard *card;
int i; int i;
// Get all the addressbooks // Get all the addressbooks
contactFolders = [[[context activeUser] homeFolderInContext: context] contactFolders = [[[context activeUser] homeFolderInContext: context]
lookupName: @"Contacts" lookupName: @"Contacts"
@ -1857,7 +1794,7 @@ static NSString *userAgent = nil;
parser = [NGMailAddressParser mailAddressParserWithString: recipient]; parser = [NGMailAddressParser mailAddressParserWithString: recipient];
parsedRecipient = [parser parse]; parsedRecipient = [parser parse];
emailAddress = [parsedRecipient address]; emailAddress = [parsedRecipient address];
matchingContacts = [contactFolders allContactsFromFilter: emailAddress matchingContacts = [contactFolders allContactsFromFilter: emailAddress
excludeGroups: YES excludeGroups: YES
excludeLists: YES]; excludeLists: YES];
@ -1869,13 +1806,13 @@ static NSString *userAgent = nil;
addressBook = [ud selectedAddressBook]; addressBook = [ud selectedAddressBook];
folder = [contactFolders lookupName: addressBook inContext: context acquire: NO]; folder = [contactFolders lookupName: addressBook inContext: context acquire: NO];
uid = [folder globallyUniqueObjectId]; uid = [folder globallyUniqueObjectId];
if (folder && uid) if (folder && uid)
{ {
card = [NGVCard cardWithUid: uid]; card = [NGVCard cardWithUid: uid];
[card addEmail: emailAddress types: nil]; [card addEmail: emailAddress types: nil];
[card setFn: [parsedRecipient displayName]]; [card setFn: [parsedRecipient displayName]];
newContact = [SOGoContactGCSEntry objectWithName: uid newContact = [SOGoContactGCSEntry objectWithName: uid
inContainer: folder]; inContainer: folder];
[newContact setIsNew: YES]; [newContact setIsNew: YES];
@ -1921,7 +1858,7 @@ static NSString *userAgent = nil;
r1 = [cleaned_message rangeOfCString: "\r\nbcc: " r1 = [cleaned_message rangeOfCString: "\r\nbcc: "
options: 0 options: 0
range: NSMakeRange(0,limit)]; range: NSMakeRange(0,limit)];
if (r1.location != NSNotFound) if (r1.location != NSNotFound)
{ {
// We search for the first \r\n AFTER the Bcc: header and // We search for the first \r\n AFTER the Bcc: header and

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>
@ -122,10 +126,10 @@ static BOOL debugSoParts = NO;
- (NSString *) keyExtensionForPart: (id) _partInfo - (NSString *) keyExtensionForPart: (id) _partInfo
{ {
NSString *mt, *st; NSString *mt, *st;
if (_partInfo == nil) if (_partInfo == nil)
return nil; return nil;
mt = [_partInfo valueForKey: @"type"]; mt = [_partInfo valueForKey: @"type"];
st = [[_partInfo valueForKey: @"subtype"] lowercaseString]; st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
if ([mt isEqualToString: @"text"]) { if ([mt isEqualToString: @"text"]) {
@ -140,7 +144,7 @@ static BOOL debugSoParts = NO;
if ([st isEqualToString: @"pgp-signature"]) if ([st isEqualToString: @"pgp-signature"])
return @".asc"; return @".asc";
} }
return nil; return nil;
} }
@ -149,18 +153,18 @@ static BOOL debugSoParts = NO;
NSMutableArray *ma; NSMutableArray *ma;
NSArray *parts; NSArray *parts;
unsigned i, count; unsigned i, count;
parts = [[self bodyStructure] valueForKey: @"parts"]; parts = [[self bodyStructure] valueForKey: @"parts"];
if (![parts isNotNull]) if (![parts isNotNull])
return nil; return nil;
if ((count = [parts count]) == 0) if ((count = [parts count]) == 0)
return nil; return nil;
for (i = 0, ma = nil; i < count; i++) { for (i = 0, ma = nil; i < count; i++) {
NSString *key, *ext; NSString *key, *ext;
id part; id part;
BOOL hasParts; BOOL hasParts;
part = [parts objectAtIndex:i]; part = [parts objectAtIndex:i];
hasParts = [part valueForKey: @"parts"] != nil ? YES:NO; hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
if ((hasParts && !_withParts) || (_withParts && !hasParts)) if ((hasParts && !_withParts) || (_withParts && !hasParts))
@ -168,7 +172,7 @@ static BOOL debugSoParts = NO;
if (ma == nil) if (ma == nil)
ma = [NSMutableArray arrayWithCapacity:count - i]; ma = [NSMutableArray arrayWithCapacity:count - i];
ext = [self keyExtensionForPart:part]; ext = [self keyExtensionForPart:part];
key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ((id)ext?(id)ext: (id)@"")]; key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ((id)ext?(id)ext: (id)@"")];
[ma addObject:key]; [ma addObject:key];
@ -204,15 +208,15 @@ static BOOL debugSoParts = NO;
{ {
static NSArray *existsKey = nil; static NSArray *existsKey = nil;
id msgs; id msgs;
if (coreInfos != nil) /* if we have coreinfos, we can use them */ if (coreInfos != nil) /* if we have coreinfos, we can use them */
return [coreInfos isNotNull]; return [coreInfos isNotNull];
/* otherwise fetch something really simple */ /* otherwise fetch something really simple */
if (existsKey == nil) /* we use size, other suggestions? */ if (existsKey == nil) /* we use size, other suggestions? */
existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil]; existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
msgs = [self fetchParts:existsKey]; // returns dict msgs = [self fetchParts:existsKey]; // returns dict
msgs = [msgs valueForKey: @"fetch"]; msgs = [msgs valueForKey: @"fetch"];
return [msgs count] > 0 ? YES : NO; return [msgs count] > 0 ? YES : NO;
@ -229,7 +233,7 @@ static BOOL debugSoParts = NO;
if (heavyDebug) if (heavyDebug)
[self logWithFormat: @"M: %@", msgs]; [self logWithFormat: @"M: %@", msgs];
msgs = [msgs valueForKey: @"fetch"]; msgs = [msgs valueForKey: @"fetch"];
// We MUST honor untagged IMAP responses here otherwise we could // We MUST honor untagged IMAP responses here otherwise we could
// return really borken and nasty results. // return really borken and nasty results.
if ([msgs count] > 0) if ([msgs count] > 0)
@ -237,7 +241,7 @@ static BOOL debugSoParts = NO;
for (i = 0; i < [msgs count]; i++) for (i = 0; i < [msgs count]; i++)
{ {
coreInfos = [msgs objectAtIndex: i]; coreInfos = [msgs objectAtIndex: i];
if ([[coreInfos objectForKey: @"uid"] intValue] == [[self nameInContainer] intValue]) if ([[coreInfos objectForKey: @"uid"] intValue] == [[self nameInContainer] intValue])
break; break;
@ -337,13 +341,13 @@ static BOOL debugSoParts = NO;
{ {
NGMimeMessageParser *parser; NGMimeMessageParser *parser;
NSData *data; NSData *data;
if (headerPart != nil) if (headerPart != nil)
return [headerPart isNotNull] ? headerPart : nil; return [headerPart isNotNull] ? headerPart : nil;
if ([(data = [self mailHeaderData]) length] == 0) if ([(data = [self mailHeaderData]) length] == 0)
return nil; return nil;
// TODO: do we need to set some delegate method which stops parsing the body? // TODO: do we need to set some delegate method which stops parsing the body?
parser = [[NGMimeMessageParser alloc] init]; parser = [[NGMimeMessageParser alloc] init];
headerPart = [[parser parsePartFromData:data] retain]; headerPart = [[parser parsePartFromData:data] retain];
@ -372,21 +376,21 @@ static BOOL debugSoParts = NO;
if (![_path isNotNull]) if (![_path isNotNull])
return nil; return nil;
if ((info = [self bodyStructure]) == nil) { if ((info = [self bodyStructure]) == nil) {
[self errorWithFormat: @"got no body part structure!"]; [self errorWithFormat: @"got no body part structure!"];
return nil; return nil;
} }
/* ensure array argument */ /* ensure array argument */
if ([_path isKindOfClass:[NSString class]]) { if ([_path isKindOfClass:[NSString class]]) {
if ([_path length] == 0 || [_path isEqualToString: @"text"]) if ([_path length] == 0 || [_path isEqualToString: @"text"])
return info; return info;
_path = [_path componentsSeparatedByString: @"."]; _path = [_path componentsSeparatedByString: @"."];
} }
// deal with mails of type text/calendar // deal with mails of type text/calendar
if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"text"] && if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"text"] &&
[[[info valueForKey: @"subtype"] lowercaseString] isEqualToString: @"calendar"]) [[[info valueForKey: @"subtype"] lowercaseString] isEqualToString: @"calendar"])
@ -399,12 +403,12 @@ static BOOL debugSoParts = NO;
if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"application"]) if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"application"])
return info; 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 Remember that we need special processing for message/rfc822 which maps the
namespace of multiparts directly into the main namespace. namespace of multiparts directly into the main namespace.
TODO(hh): no I don't remember, please explain in more detail! TODO(hh): no I don't remember, please explain in more detail!
*/ */
pe = [_path objectEnumerator]; pe = [_path objectEnumerator];
@ -412,7 +416,7 @@ static BOOL debugSoParts = NO;
unsigned idx; unsigned idx;
NSArray *parts; NSArray *parts;
NSString *mt; NSString *mt;
[self debugWithFormat: @"check PATH: %@", p]; [self debugWithFormat: @"check PATH: %@", p];
idx = [p intValue] - 1; idx = [p intValue] - 1;
@ -421,7 +425,7 @@ static BOOL debugSoParts = NO;
if ([mt isEqualToString: @"message"]) { if ([mt isEqualToString: @"message"]) {
/* we have special behaviour for message types */ /* we have special behaviour for message types */
id body; id body;
if ((body = [info valueForKey: @"body"]) != nil) { if ((body = [info valueForKey: @"body"]) != nil) {
mt = [body valueForKey: @"type"]; mt = [body valueForKey: @"type"];
if ([mt isEqualToString: @"multipart"]) if ([mt isEqualToString: @"multipart"])
@ -430,10 +434,10 @@ static BOOL debugSoParts = NO;
parts = [NSArray arrayWithObject:body]; parts = [NSArray arrayWithObject:body];
} }
} }
if (idx >= [parts count]) { if (idx >= [parts count]) {
[self errorWithFormat: [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]; (idx + 1), [parts count], info];
return nil; return nil;
} }
@ -448,40 +452,40 @@ static BOOL debugSoParts = NO;
{ {
NSData *content; NSData *content;
id result, fullResult; id result, fullResult;
// We avoid using RFC822 here as the part name as it'll flag the message as Seen // We avoid using RFC822 here as the part name as it'll flag the message as Seen
fullResult = [self fetchParts: [NSArray arrayWithObject: @"BODY.PEEK[]"]]; fullResult = [self fetchParts: [NSArray arrayWithObject: @"BODY.PEEK[]"]];
if (fullResult == nil) if (fullResult == nil)
return nil; return nil;
if ([fullResult isKindOfClass: [NSException class]]) if ([fullResult isKindOfClass: [NSException class]])
return fullResult; return fullResult;
/* extract fetch result */ /* extract fetch result */
result = [fullResult valueForKey: @"fetch"]; result = [fullResult valueForKey: @"fetch"];
if (![result isKindOfClass:[NSArray class]]) { if (![result isKindOfClass:[NSArray class]]) {
[self logWithFormat: [self logWithFormat:
@"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
fullResult]; fullResult];
return [NSException exceptionWithHTTPStatus:500 /* server error */ return [NSException exceptionWithHTTPStatus:500 /* server error */
reason: @"unexpected IMAP4 result"]; reason: @"unexpected IMAP4 result"];
} }
if ([result count] == 0) if ([result count] == 0)
return nil; return nil;
result = [result objectAtIndex:0]; result = [result objectAtIndex:0];
/* extract message */ /* extract message */
if ((content = [[result valueForKey: @"body[]"] valueForKey: @"data"]) == nil) { if ((content = [[result valueForKey: @"body[]"] valueForKey: @"data"]) == nil) {
[self logWithFormat: [self logWithFormat:
@"ERROR: unexpected IMAP4 result (missing 'message'): %@", @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
result]; result];
return [NSException exceptionWithHTTPStatus:500 /* server error */ return [NSException exceptionWithHTTPStatus:500 /* server error */
reason: @"unexpected IMAP4 result"]; reason: @"unexpected IMAP4 result"];
} }
return [[content copy] autorelease]; return [[content copy] autorelease];
} }
@ -507,7 +511,7 @@ static BOOL debugSoParts = NO;
[s autorelease]; [s autorelease];
else else
[self logWithFormat: [self logWithFormat:
@"ERROR: could not convert data of length %d to string", @"ERROR: could not convert data of length %d to string",
[content length]]; [content length]];
} }
else else
@ -529,11 +533,11 @@ static BOOL debugSoParts = NO;
withPeek: (BOOL) withPeek withPeek: (BOOL) withPeek
parentMultipart: (NSString *) parentMPart parentMultipart: (NSString *) parentMPart
{ {
/* /*
This is used to collect the set of IMAP4 fetch-keys required to fetch 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 the basic parts of the body structure. That is, to fetch all parts which
are displayed 'inline' in a single IMAP4 fetch. are displayed 'inline' in a single IMAP4 fetch.
The method calls itself recursively to walk the body structure. The method calls itself recursively to walk the body structure.
*/ */
NSArray *parts; NSArray *parts;
@ -555,7 +559,7 @@ static BOOL debugSoParts = NO;
multipart = mimeType; multipart = mimeType;
else else
multipart = parentMPart; multipart = parentMPart;
if ([types containsObject: mimeType]) if ([types containsObject: mimeType])
{ {
if ([p length] > 0) if ([p length] > 0)
@ -581,9 +585,9 @@ static BOOL debugSoParts = NO;
sp = (([p length] > 0) sp = (([p length] > 0)
? (id)[p stringByAppendingFormat: @".%d", i + 1] ? (id)[p stringByAppendingFormat: @".%d", i + 1]
: (id)[NSString stringWithFormat: @"%d", i + 1]); : (id)[NSString stringWithFormat: @"%d", i + 1]);
childInfo = [parts objectAtIndex: i]; childInfo = [parts objectAtIndex: i];
[self addRequiredKeysOfStructure: childInfo [self addRequiredKeysOfStructure: childInfo
path: sp path: sp
toArray: keys toArray: keys
@ -591,7 +595,7 @@ static BOOL debugSoParts = NO;
withPeek: withPeek withPeek: withPeek
parentMultipart: multipart]; parentMultipart: multipart];
} }
/* check body */ /* check body */
body = [info objectForKey: @"body"]; body = [info objectForKey: @"body"];
if (body) if (body)
@ -651,7 +655,7 @@ static BOOL debugSoParts = NO;
[self addRequiredKeysOfStructure: [self bodyStructure] [self addRequiredKeysOfStructure: [self bodyStructure]
path: @"" path: @""
toArray: ma toArray: ma
acceptedTypes: types acceptedTypes: types
withPeek: YES]; withPeek: YES];
@ -665,7 +669,7 @@ static BOOL debugSoParts = NO;
unsigned i, count; unsigned i, count;
NSArray *results; NSArray *results;
id result; id result;
[self debugWithFormat: @"fetch keys: %@", _fetchKeys]; [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
result = [self fetchParts: [_fetchKeys objectsForKey: @"key" result = [self fetchParts: [_fetchKeys objectsForKey: @"key"
@ -681,7 +685,7 @@ static BOOL debugSoParts = NO;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
NSString *key; NSString *key;
NSData *data; NSData *data;
key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"]; key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
// We'll ask for the body.peek[] but SOPE returns us body[] responses // 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["]) if ([key hasPrefix: @"body.peek["])
key = [NSString stringWithFormat: @"body[%@", [key substringFromIndex: 10]]; key = [NSString stringWithFormat: @"body[%@", [key substringFromIndex: 10]];
data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
objectForKey: @"data"]; objectForKey: @"data"];
if (![data isNotNull]) { if (![data isNotNull]) {
[self errorWithFormat: @"got no data for key: %@", key]; [self errorWithFormat: @"got no data for key: %@", key];
continue; continue;
} }
if ([key isEqualToString: @"body[text]"]) if ([key isEqualToString: @"body[text]"])
key = @""; // see key collector for explanation (TODO: where?) key = @""; // see key collector for explanation (TODO: where?)
else if ([key hasPrefix: @"body["]) { else if ([key hasPrefix: @"body["]) {
NSRange r; NSRange r;
key = [key substringFromIndex:5]; key = [key substringFromIndex:5];
r = [key rangeOfString: @"]"]; r = [key rangeOfString: @"]"];
if (r.length > 0) if (r.length > 0)
@ -913,13 +917,167 @@ 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
{ {
NSString *charset, *s; NSString *charset, *s;
NSData *mailData; NSData *mailData;
if ([_data isNotNull]) if ([_data isNotNull])
{ {
mailData mailData
@ -934,13 +1092,13 @@ static BOOL debugSoParts = NO;
{ {
s = [NSString stringWithData: mailData usingEncodingNamed: charset]; s = [NSString stringWithData: mailData usingEncodingNamed: charset];
} }
// If it has failed, we try at least using UTF-8. Normally, this can NOT fail. // 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 // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's
// the case // the case
if (!s) if (!s)
s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease]; s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease];
if (!s) if (!s)
s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease]; 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 The fetched parts are NSData objects, this method converts them into
NSString objects based on the information inside the bodystructure. 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 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. The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
*/ */
NSDictionary *datas; NSDictionary *datas;
if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil) if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
return nil; return nil;
if ([datas isKindOfClass:[NSException class]]) if ([datas isKindOfClass:[NSException class]])
return datas; return datas;
return [self stringifyTextParts:datas]; return [self stringifyTextParts:datas];
} }
@ -1065,13 +1223,13 @@ static BOOL debugSoParts = NO;
acquire: (BOOL) _flag acquire: (BOOL) _flag
{ {
id obj; id obj;
/* first check attributes directly bound to the application */ /* first check attributes directly bound to the application */
if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil) if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
return obj; return obj;
/* lookup body part */ /* lookup body part */
if ([self isBodyPartKey:_key]) { if ([self isBodyPartKey:_key]) {
if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) { if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
if (debugSoParts) if (debugSoParts)
@ -1087,7 +1245,7 @@ static BOOL debugSoParts = NO;
[obj setAsAttachment]; [obj setAsAttachment];
return obj; return obj;
} }
/* return 404 to stop acquisition */ /* return 404 to stop acquisition */
return [NSException exceptionWithHTTPStatus:404 /* Not Found */ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
reason: @"Did not find mail method or part-reference!"]; reason: @"Did not find mail method or part-reference!"];
@ -1131,7 +1289,7 @@ static BOOL debugSoParts = NO;
newName: (NSString *) _name newName: (NSString *) _name
inContext: (id)_ctx inContext: (id)_ctx
{ {
/* /*
Note: this is special because we create SOGoMailObject's even if they do Note: this is special because we create SOGoMailObject's even if they do
not exist (for performance reasons). not exist (for performance reasons).
@ -1181,7 +1339,7 @@ static BOOL debugSoParts = NO;
NSException *error; NSException *error;
WOResponse *r; WOResponse *r;
NSData *content; NSData *content;
if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) { if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
/* check whether the mail still exists */ /* check whether the mail still exists */
if (![self doesMailExist]) { if (![self doesMailExist]) {
@ -1190,7 +1348,7 @@ static BOOL debugSoParts = NO;
} }
return error; /* return 304 or 416 */ return error; /* return 304 or 416 */
} }
content = [self content]; content = [self content];
if ([content isKindOfClass:[NSException class]]) if ([content isKindOfClass:[NSException class]])
return content; return content;
@ -1198,7 +1356,7 @@ static BOOL debugSoParts = NO;
return [NSException exceptionWithHTTPStatus:404 /* Not Found */ return [NSException exceptionWithHTTPStatus:404 /* Not Found */
reason: @"did not find IMAP4 message"]; reason: @"did not find IMAP4 message"];
} }
r = [(WOContext *)_ctx response]; r = [(WOContext *)_ctx response];
[r setHeader: @"message/rfc822" forKey: @"content-type"]; [r setHeader: @"message/rfc822" forKey: @"content-type"];
[r setContent:content]; [r setContent:content];
@ -1257,19 +1415,19 @@ static BOOL debugSoParts = NO;
inContext: _ctx]) inContext: _ctx])
{ {
/* b) mark deleted */ /* b) mark deleted */
error = [[self imap4Connection] markURLDeleted: [self imap4URL]]; error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
if (error != nil) return error; if (error != nil) return error;
[self flushMailCaches]; [self flushMailCaches];
} }
return nil; return nil;
} }
- (NSException *) delete - (NSException *) delete
{ {
/* /*
Note: delete is different to DELETEAction: for mails! The 'delete' runs Note: delete is different to DELETEAction: for mails! The 'delete' runs
either flags a message as deleted or moves it to the Trash while either flags a message as deleted or moves it to the Trash while
the DELETEAction: really deletes a message (by flagging it as the DELETEAction: really deletes a message (by flagging it as
@ -1279,7 +1437,7 @@ static BOOL debugSoParts = NO;
NSException *error; NSException *error;
// TODO: check for safe HTTP method // TODO: check for safe HTTP method
error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
return error; return error;
} }
@ -1287,15 +1445,15 @@ static BOOL debugSoParts = NO;
- (id) DELETEAction: (id) _ctx - (id) DELETEAction: (id) _ctx
{ {
NSException *error; NSException *error;
// TODO: ensure safe HTTP method // TODO: ensure safe HTTP method
error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
if (error != nil) return error; if (error != nil) return error;
error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]]; error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
if (error != nil) return error; // TODO: unflag as deleted? if (error != nil) return error; // TODO: unflag as deleted?
return [NSNumber numberWithBool:YES]; /* delete was successful */ return [NSNumber numberWithBool:YES]; /* delete was successful */
} }
@ -1304,20 +1462,20 @@ static BOOL debugSoParts = NO;
- (BOOL) isMailingListMail - (BOOL) isMailingListMail
{ {
NSDictionary *h; NSDictionary *h;
if ((h = [self mailHeaders]) == nil) if ((h = [self mailHeaders]) == nil)
return NO; return NO;
return [[h objectForKey: @"list-id"] isNotEmpty]; return [[h objectForKey: @"list-id"] isNotEmpty];
} }
- (BOOL) isVirusScanned - (BOOL) isVirusScanned
{ {
NSDictionary *h; NSDictionary *h;
if ((h = [self mailHeaders]) == nil) if ((h = [self mailHeaders]) == nil)
return NO; return NO;
if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO; if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO; if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
return YES; return YES;
@ -1329,10 +1487,10 @@ static BOOL debugSoParts = NO;
/* Note: not very tolerant on embedded commands and <> */ /* Note: not very tolerant on embedded commands and <> */
// TODO: does not really belong here, should be a header-field-parser // TODO: does not really belong here, should be a header-field-parser
NSRange r; NSRange r;
if (![_value isNotEmpty]) if (![_value isNotEmpty])
return nil; return nil;
if ([_value isKindOfClass:[NSArray class]]) { if ([_value isKindOfClass:[NSArray class]]) {
NSEnumerator *e; NSEnumerator *e;
id value; id value;
@ -1344,10 +1502,10 @@ static BOOL debugSoParts = NO;
} }
return nil; return nil;
} }
if (![_value isKindOfClass:[NSString class]]) if (![_value isKindOfClass:[NSString class]])
return nil; return nil;
/* check for commas in string values */ /* check for commas in string values */
r = [_value rangeOfString: @","]; r = [_value rangeOfString: @","];
if (r.length > 0) { if (r.length > 0) {
@ -1358,7 +1516,7 @@ static BOOL debugSoParts = NO;
/* value qualifies */ /* value qualifies */
if (![(NSString *)_value hasPrefix:_prefix]) if (![(NSString *)_value hasPrefix:_prefix])
return nil; return nil;
/* unquote */ /* unquote */
if ([_value characterAtIndex:0] == '<') { if ([_value characterAtIndex:0] == '<') {
r = [_value rangeOfString: @">"]; r = [_value rangeOfString: @">"];
@ -1456,7 +1614,7 @@ static BOOL debugSoParts = NO;
if (property) if (property)
{ {
parts = [NSArray arrayWithObject: property]; parts = [NSArray arrayWithObject: property];
msgs = [self fetchParts: parts]; msgs = [self fetchParts: parts];
msgs = [msgs valueForKey: @"fetch"]; msgs = [msgs valueForKey: @"fetch"];
if ([msgs count]) { if ([msgs count]) {
@ -1512,7 +1670,7 @@ static BOOL debugSoParts = NO;
// date already exists, but this one is the correct format // date already exists, but this one is the correct format
- (NSString *) davDate - (NSString *) davDate
{ {
return [[self date] rfc822DateString]; return [[self date] rfc822DateString];
} }
- (BOOL) hasAttachment - (BOOL) hasAttachment
@ -1597,12 +1755,12 @@ static BOOL debugSoParts = NO;
if ([fetch count]) if ([fetch count])
{ {
data = [fetch objectForKey: @"header"]; data = [fetch objectForKey: @"header"];
value = [[NSString alloc] initWithData: data value = [[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding]; encoding: NSUTF8StringEncoding];
range = [value rangeOfString: @"received:" range = [value rangeOfString: @"received:"
options: NSCaseInsensitiveSearch options: NSCaseInsensitiveSearch
range: NSMakeRange (10, [value length] - 11)]; range: NSMakeRange (10, [value length] - 11)];
if (range.length if (range.length
&& range.location < [value length] && range.location < [value length]
&& range.length < [value length]) && range.length < [value length])
{ {

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

@ -659,7 +659,7 @@
AddressBook.prototype.$deleteCards = function(cards) { AddressBook.prototype.$deleteCards = function(cards) {
var _this = this, var _this = this,
ids = _.map(cards, 'id'); ids = _.map(cards, 'id');
return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: ids}).then(function() { return AddressBook.$$resource.post(this.id, 'batchDelete', {uids: ids}).then(function() {
_this.$_deleteCards(ids); _this.$_deleteCards(ids);
}); });
@ -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);
}; };
/** /**
@ -742,7 +748,7 @@
return _this.$_deleteMessages(uids, messages); return _this.$_deleteMessages(uids, messages);
}); });
}; };
/** /**
* @function $reset * @function $reset
* @memberof Mailbox.prototype * @memberof Mailbox.prototype

View File

@ -444,9 +444,9 @@
* @function $imipAction * @function $imipAction
* @memberof Message.prototype * @memberof Message.prototype
* @desc Perform IMIP actions on the current message. * @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 {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) { Message.prototype.$imipAction = function(path, action, data) {
var _this = this; var _this = this;
@ -657,7 +657,7 @@
/** /**
* @function $unwrap * @function $unwrap
* @memberof Message.prototype * @memberof Message.prototype
* @desc Unwrap a promise. * @desc Unwrap a promise.
* @param {promise} futureMessageData - a promise of some of the Message's data * @param {promise} futureMessageData - a promise of some of the Message's data
*/ */
Message.prototype.$unwrap = function(futureMessageData) { Message.prototype.$unwrap = function(futureMessageData) {
@ -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);
}; };
/** /**