From c160edf20abdb728135eb624e28ad490b8892153 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Mon, 27 Jan 2014 15:09:22 -0500 Subject: [PATCH] Add support for Sieve body extension --- NEWS | 2 + SoObjects/Mailer/SOGoMailAccount.m | 5 +- SoObjects/SOGo/SOGoSieveManager.h | 8 +- SoObjects/SOGo/SOGoSieveManager.m | 134 +++++++++++++++-------- UI/PreferencesUI/UIxPreferences.m | 19 +++- UI/WebServerResources/UIxFilterEditor.js | 12 +- 6 files changed, 115 insertions(+), 65 deletions(-) diff --git a/NEWS b/NEWS index 6f526b790..1770ec45e 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ New features mail editor; will also now display progress of uploads - new popup menu to download all attachments of a mail - move & copy messages between different accounts + - support for the Sieve 'body' extension (mail filter based on the body content) Enhancements - we now automatically convert into file attachments @@ -17,6 +18,7 @@ Enhancements - format time in attendees invitation window according to the user's locale - improved IE11 support - respect signature placement when forwarding a message + - respect Sieve server capabilities Bug fixes - don't load 'background' attribute (#2437) diff --git a/SoObjects/Mailer/SOGoMailAccount.m b/SoObjects/Mailer/SOGoMailAccount.m index 276bc208a..2beb53acb 100644 --- a/SoObjects/Mailer/SOGoMailAccount.m +++ b/SoObjects/Mailer/SOGoMailAccount.m @@ -311,10 +311,7 @@ static NSString *inboxFolderName = @"INBOX"; manager = [SOGoSieveManager sieveManagerForUser: [context activeUser]]; - return [manager updateFiltersForLogin: [[self imap4URL] user] - authname: [[self imap4URL] user] - password: [self imap4PasswordRenewed: NO] - account: self]; + return [manager updateFiltersForAccount: self]; } diff --git a/SoObjects/SOGo/SOGoSieveManager.h b/SoObjects/SOGo/SOGoSieveManager.h index 2f180426a..783a948d8 100644 --- a/SoObjects/SOGo/SOGoSieveManager.h +++ b/SoObjects/SOGo/SOGoSieveManager.h @@ -29,6 +29,7 @@ @class NSDictionary; @class NSMutableArray; @class NSString; +@class NGSieveClient; @class SOGoMailAccount; @class SOGoUser; @@ -45,10 +46,9 @@ - (NSString *) sieveScriptWithRequirements: (NSMutableArray *) newRequirements; - (NSString *) lastScriptError; -- (BOOL) updateFiltersForLogin: (NSString *) theLogin - authname: (NSString *) theAuthName - password: (NSString *) thePassword - account: (SOGoMailAccount *) theAccount; +- (NGSieveClient *) clientForAccount: (SOGoMailAccount *) theAccount; + +- (BOOL) updateFiltersForAccount: (SOGoMailAccount *) theAccount; @end diff --git a/SoObjects/SOGo/SOGoSieveManager.m b/SoObjects/SOGo/SOGoSieveManager.m index 603e21e28..99a55bf03 100644 --- a/SoObjects/SOGo/SOGoSieveManager.m +++ b/SoObjects/SOGo/SOGoSieveManager.m @@ -42,6 +42,7 @@ typedef enum { UIxFilterFieldTypeAddress, UIxFilterFieldTypeHeader, + UIxFilterFieldTypeBody, UIxFilterFieldTypeSize, } UIxFilterFieldType; @@ -50,6 +51,7 @@ static NSArray *sieveSizeOperators = nil; static NSMutableDictionary *fieldTypes = nil; static NSDictionary *sieveFields = nil; static NSDictionary *sieveFlags = nil; +static NSDictionary *typeRequirements = nil; static NSDictionary *operatorRequirements = nil; static NSMutableDictionary *methodRequirements = nil; static NSString *sieveScriptName = @"sogo"; @@ -136,13 +138,13 @@ static NSString *sieveScriptName = @"sogo"; fieldTypes = [NSMutableDictionary new]; fields = [NSArray arrayWithObjects: @"to", @"cc", @"to_or_cc", @"from", nil]; - [fieldTypes setObject: [NSNumber - numberWithInt: UIxFilterFieldTypeAddress] + [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeAddress] forKeys: fields]; fields = [NSArray arrayWithObjects: @"header", @"subject", nil]; - [fieldTypes setObject: [NSNumber - numberWithInt: UIxFilterFieldTypeHeader] + [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeHeader] forKeys: fields]; + [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeBody] + forKey: @"body"]; [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeSize] forKey: @"size"]; } @@ -177,6 +179,14 @@ static NSString *sieveScriptName = @"sogo"; nil]; [sieveFlags retain]; } + if (!typeRequirements) + { + typeRequirements + = [NSDictionary dictionaryWithObjectsAndKeys: + @"body", [NSNumber numberWithInt: UIxFilterFieldTypeBody], + nil]; + [typeRequirements retain]; + } if (!operatorRequirements) { operatorRequirements @@ -252,7 +262,7 @@ static NSString *sieveScriptName = @"sogo"; andType: (UIxFilterFieldType *) type { NSNumber *fieldType; - NSString *jsonField, *customHeader; + NSString *jsonField, *customHeader, *requirement; jsonField = [rule objectForKey: @"field"]; if (jsonField) @@ -270,10 +280,15 @@ static NSString *sieveScriptName = @"sogo"; scriptError = (@"Pseudo-header field 'header' without" @" 'custom_header' parameter."); } - else if ([jsonField isEqualToString: @"size"]) + else if ([jsonField isEqualToString: @"body"] || + [jsonField isEqualToString: @"size"]) *field = nil; else *field = [sieveFields objectForKey: jsonField]; + + requirement = [typeRequirements objectForKey: fieldType]; + if (requirement) + [requirements addObjectUniquely: requirement]; } else scriptError @@ -332,6 +347,7 @@ static NSString *sieveScriptName = @"sogo"; if (type == UIxFilterFieldTypeSize) rc = [sieveSizeOperators containsObject: operator]; else + // Header and Body types rc = (![sieveSizeOperators containsObject: operator] && [sieveOperators containsObject: operator]); @@ -370,17 +386,23 @@ static NSString *sieveScriptName = @"sogo"; sieveRule = [NSMutableString stringWithCapacity: 100]; if (revert) [sieveRule appendString: @"not "]; + if (type == UIxFilterFieldTypeAddress) [sieveRule appendString: @"address "]; else if (type == UIxFilterFieldTypeHeader) [sieveRule appendString: @"header "]; + else if (type == UIxFilterFieldTypeBody) + [sieveRule appendString: @"body :text "]; else if (type == UIxFilterFieldTypeSize) [sieveRule appendString: @"size "]; [sieveRule appendFormat: @":%@ ", operator]; + if (type == UIxFilterFieldTypeSize) [sieveRule appendFormat: @"%@K", value]; - else + else if (field) [sieveRule appendFormat: @"%@ %@", field, value]; + else + [sieveRule appendFormat: @"%@", value]; return sieveRule; } @@ -529,24 +551,21 @@ static NSString *sieveScriptName = @"sogo"; { if ([match isEqualToString: @"all"] || [match isEqualToString: @"any"]) { - sieveRules - = [self _extractSieveRules: [newScript objectForKey: @"rules"]]; + sieveRules = [self _extractSieveRules: [newScript objectForKey: @"rules"]]; if (sieveRules) [sieveText appendFormat: @"if %@of (%@) {\r\n", - match, - [sieveRules componentsJoinedByString: @", "]]; + match, + [sieveRules componentsJoinedByString: @", "]]; else scriptError = [NSString stringWithFormat: - @"Test '%@' used without any" + @"Test '%@' used without any" @" specified rule", match]; } else - scriptError = [NSString stringWithFormat: @"Bad test: %@", - match]; + scriptError = [NSString stringWithFormat: @"Bad test: %@", match]; } - sieveActions = [self _extractSieveActions: - [newScript objectForKey: @"actions"]]; + sieveActions = [self _extractSieveActions: [newScript objectForKey: @"actions"]]; if ([sieveActions count]) [sieveText appendFormat: @" %@;\r\n", [sieveActions componentsJoinedByString: @";\r\n "]]; @@ -604,34 +623,25 @@ static NSString *sieveScriptName = @"sogo"; // // // -- (BOOL) updateFiltersForLogin: (NSString *) theLogin - authname: (NSString *) theAuthName - password: (NSString *) thePassword - account: (SOGoMailAccount *) theAccount +- (NGSieveClient *) clientForAccount: (SOGoMailAccount *) theAccount { - NSMutableArray *req; - NSMutableString *script, *header; - NSDictionary *result, *values; - SOGoUserDefaults *ud; + NSDictionary *result; + NSString *login, *authname, *password; SOGoDomainDefaults *dd; NGSieveClient *client; - NSString *filterScript, *v, *sieveServer, *sieveScheme, *sieveQuery, *imapServer; + NSString *sieveServer, *sieveScheme, *sieveQuery, *imapServer; NSURL *url, *cUrl; - int sievePort; - BOOL b, connected; + BOOL connected; dd = [user domainDefaults]; - if (!([dd sieveScriptsEnabled] || [dd vacationEnabled] || [dd forwardEnabled])) - return YES; - - req = [NSMutableArray arrayWithCapacity: 15]; - ud = [user userDefaults]; - connected = YES; - b = NO; - + // Extract credentials from mail account + login = [[theAccount imap4URL] user]; + authname = [[theAccount imap4URL] user]; + password = [theAccount imap4PasswordRenewed: NO]; + // We connect to our Sieve server and check capabilities, in order // to generate the right script, based on capabilities // @@ -687,20 +697,20 @@ static NSString *sieveScriptName = @"sogo"; sieveScheme, sieveServer, sievePort, sieveQuery]]; client = [[NGSieveClient alloc] initWithURL: url]; - + if (!client) { NSLog(@"Sieve connection failed on %@", [url description]); - return NO; + return nil; } - - if (!thePassword) { + + if (!password) { [client closeConnection]; - return NO; + return nil; } NS_DURING { - result = [client login: theLogin authname: theAuthName password: thePassword]; + result = [client login: login authname: authname password: password]; } NS_HANDLER { @@ -711,22 +721,50 @@ static NSString *sieveScriptName = @"sogo"; if (!connected) { NSLog(@"Sieve connection failed on %@", [url description]); - return NO; + return nil; } if (![[result valueForKey:@"result"] boolValue]) { NSLog(@"failure. Attempting with a renewed password (no authname supported)"); - thePassword = [theAccount imap4PasswordRenewed: YES]; - result = [client login: theLogin password: thePassword]; + password = [theAccount imap4PasswordRenewed: YES]; + result = [client login: login password: password]; } - + if (![[result valueForKey:@"result"] boolValue]) { NSLog(@"Could not login '%@' on Sieve server: %@: %@", - theLogin, client, result); + login, client, result); [client closeConnection]; - return NO; + return nil; } - + + return client; +} + +// +// +// +- (BOOL) updateFiltersForAccount: (SOGoMailAccount *) theAccount +{ + NSMutableArray *req; + NSMutableString *script, *header; + NSDictionary *result, *values; + SOGoUserDefaults *ud; + SOGoDomainDefaults *dd; + NGSieveClient *client; + NSString *filterScript, *v; + BOOL b; + + dd = [user domainDefaults]; + if (!([dd sieveScriptsEnabled] || [dd vacationEnabled] || [dd forwardEnabled])) + return YES; + + req = [NSMutableArray arrayWithCapacity: 15]; + ud = [user userDefaults]; + + client = [self clientForAccount: theAccount]; + if (!client) + return NO; + // We adjust the "methodRequirements" based on the server's // capabilities. Cyrus exposes "imapflags" while Dovecot (and // potentially others) expose "imap4flags" as specified in RFC5332 diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 612d1eb6c..a4fc1baa9 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -41,6 +41,7 @@ #import #import #import +#import #import #import #import @@ -837,13 +838,23 @@ static NSArray *reminderValues = nil; { #warning sieve caps should be deduced from the server static NSArray *capabilities = nil; + SOGoMailAccounts *folder; + SOGoMailAccount *account; + SOGoSieveManager *manager; + NGSieveClient *client; if (!capabilities) { - capabilities = [NSArray arrayWithObjects: @"fileinto", @"reject", - @"envelope", @"vacation", @"imapflags", - @"notify", @"subaddress", @"relational", - @"comparator-i;ascii-numeric", @"regex", nil]; + folder = [[self clientObject] mailAccountsFolder: @"Mail" + inContext: context]; + account = [folder lookupName: @"0" inContext: context acquire: NO]; + manager = [SOGoSieveManager sieveManagerForUser: [context activeUser]]; + client = [manager clientForAccount: account]; + + if (client) + capabilities = [client capabilities]; + else + capabilities = [NSArray array]; [capabilities retain]; } diff --git a/UI/WebServerResources/UIxFilterEditor.js b/UI/WebServerResources/UIxFilterEditor.js index cfa8c3b38..16f74b7ca 100644 --- a/UI/WebServerResources/UIxFilterEditor.js +++ b/UI/WebServerResources/UIxFilterEditor.js @@ -63,7 +63,8 @@ function setupConstants() { "cc": _("Cc"), "to_or_cc": _("To or Cc"), "size": _("Size (Kb)"), - "header": _("Header") }; + "header": _("Header"), + "body": _("Body") }; methodLabels = { "addflag": _("Flag the message with:"), "discard": _("Discard the message"), "fileinto": _("File the message in:"), @@ -272,8 +273,10 @@ function ensureFieldRepresentation(container) { } function ensureFieldSelectRepresentation(container, fieldSpan) { - var fields - = [ "subject", "from", "to", "cc", "to_or_cc", "size", "header" ]; + var fields = [ "subject", "from", "to", "cc", "to_or_cc", "size", "header" ]; + if (sieveCapabilities.indexOf("body") > -1) { + fields.push("body"); + } var selects = fieldSpan.select("SELECT"); var select; if (selects.length) @@ -373,8 +376,7 @@ function ensureOperatorSelectRepresentation(container, operatorSpan) { var operatorOption = createElement("option", null, null, { value: operator }, null, select); - operatorOption.appendChild(document - .createTextNode(operatorLabels[operator])); + operatorOption.appendChild(document.createTextNode(operatorLabels[operator])); } operatorSpan.appendChild(select); }