/* SOGoSieveConverter.m - this file is part of SOGo * * Copyright (C) 2010 Wolfgang Sourdeau * * Author: Wolfgang Sourdeau * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import "SOGoMailAccounts.h" #import "SOGoSieveConverter.h" typedef enum { UIxFilterFieldTypeAddress, UIxFilterFieldTypeHeader, UIxFilterFieldTypeSize, } UIxFilterFieldType; static NSArray *sieveOperators = nil; static NSArray *sieveSizeOperators = nil; static NSMutableDictionary *fieldTypes = nil; static NSDictionary *sieveFields = nil; static NSDictionary *sieveFlags = nil; static NSDictionary *operatorRequirements = nil; static NSDictionary *methodRequirements = nil; @interface NSString (SOGoSieveExtension) - (NSString *) asSieveQuotedString; @end @implementation NSString (SOGoSieveExtension) - (NSString *) _asSingleLineSieveQuotedString { NSString *escapedString; escapedString = [[self stringByReplacingString: @"\\" withString: @"\\\\"] stringByReplacingString: @"\"" withString: @"\\\""]; return [NSString stringWithFormat: @"\"%@\"", escapedString]; } - (NSString *) _asMultiLineSieveQuotedString { NSArray *lines; NSMutableArray *newLines; NSString *line, *newText; int count, max; lines = [self componentsSeparatedByString: @"\n"]; max = [lines count]; newLines = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { line = [lines objectAtIndex: count]; if ([line length] > 0 && [line characterAtIndex: 0] == '.') [newLines addObject: [NSString stringWithFormat: @".%@", line]]; else [newLines addObject: line]; } newText = [NSString stringWithFormat: @"\r\n%@\r\n.\r\n", [newLines componentsJoinedByString: @"\n"]]; return newText; } - (NSString *) asSieveQuotedString { NSRange nlRange; nlRange = [self rangeOfString: @"\n"]; return ((nlRange.length > 0) ? [self _asMultiLineSieveQuotedString] : [self _asSingleLineSieveQuotedString]); } @end @implementation SOGoSieveConverter + (void) initialize { NSArray *fields; if (!sieveOperators) { sieveOperators = [NSArray arrayWithObjects: @"is", @"contains", @"matches", @"regex", @"over", @"under", nil]; [sieveOperators retain]; } if (!sieveSizeOperators) { sieveSizeOperators = [NSArray arrayWithObjects: @"over", @"under", nil]; [sieveSizeOperators retain]; } if (!fieldTypes) { fieldTypes = [NSMutableDictionary new]; fields = [NSArray arrayWithObjects: @"to", @"cc", @"to_or_cc", @"from", nil]; [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeAddress] forKeys: fields]; fields = [NSArray arrayWithObjects: @"header", @"subject", nil]; [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeHeader] forKeys: fields]; [fieldTypes setObject: [NSNumber numberWithInt: UIxFilterFieldTypeSize] forKey: @"size"]; } if (!sieveFields) { sieveFields = [NSDictionary dictionaryWithObjectsAndKeys: @"\"to\"", @"to", @"\"cc\"", @"cc", @"[\"to\", \"cc\"]", @"to_or_cc", @"\"from\"", @"from", @"\"subject\"", @"subject", nil]; [sieveFields retain]; } if (!sieveFlags) { sieveFlags = [NSDictionary dictionaryWithObjectsAndKeys: @"\\Answered", @"answered", @"\\Deleted", @"deleted", @"\\Draft", @"draft", @"\\Flagged", @"flagged", @"Junk", @"junk", @"NotJunk", @"not_junk", @"\\Seen", @"seen", @"$Label1", @"label1", @"$Label2", @"label2", @"$Label3", @"label3", @"$Label4", @"label4", @"$Label5", @"label5", nil]; [sieveFlags retain]; } if (!operatorRequirements) { operatorRequirements = [NSDictionary dictionaryWithObjectsAndKeys: @"regex", @"regex", nil]; [operatorRequirements retain]; } if (!methodRequirements) { methodRequirements = [NSDictionary dictionaryWithObjectsAndKeys: @"imapflags", @"addflag", @"imapflags", @"removeflag", @"imapflags", @"flag", @"vacation", @"vacation", @"notify", @"notify", @"fileinto", @"fileinto", @"reject", @"reject", @"regex", @"regex", nil]; [methodRequirements retain]; } } + (id) sieveConverterForUser: (SOGoUser *) newUser { SOGoSieveConverter *newConverter; newConverter = [[self alloc] initForUser: newUser]; [newConverter autorelease]; return newConverter; } - (id) init { if ((self = [super init])) { user = nil; requirements = nil; scriptError = nil; } return self; } - (id) initForUser: (SOGoUser *) newUser { if ((self = [self init])) { ASSIGN (user, newUser); } return self; } - (void) dealloc { [user release]; [requirements release]; [scriptError release]; [super dealloc]; } - (BOOL) _saveFilters { return YES; } - (BOOL) _extractRuleField: (NSString **) field fromRule: (NSDictionary *) rule andType: (UIxFilterFieldType *) type { NSNumber *fieldType; NSString *jsonField, *customHeader; jsonField = [rule objectForKey: @"field"]; if (jsonField) { fieldType = [fieldTypes objectForKey: jsonField]; if (fieldType) { *type = [fieldType intValue]; if ([jsonField isEqualToString: @"header"]) { customHeader = [rule objectForKey: @"custom_header"]; if ([customHeader length]) *field = [customHeader asSieveQuotedString]; else scriptError = (@"Pseudo-header field 'header' without" @" 'custom_header' parameter."); } else if ([jsonField isEqualToString: @"size"]) *field = nil; else *field = [sieveFields objectForKey: jsonField]; } else scriptError = [NSString stringWithFormat: @"Rule based on unknown field '%@'", *field]; } else scriptError = @"Rule without any specified field."; return (scriptError == nil); } - (BOOL) _extractRuleOperator: (NSString **) operator fromRule: (NSDictionary *) rule isNot: (BOOL *) isNot { NSString *jsonOperator, *baseOperator, *requirement; int baseLength; jsonOperator = [rule objectForKey: @"operator"]; if (jsonOperator) { *isNot = [jsonOperator hasSuffix: @"_not"]; if (*isNot) { baseLength = [jsonOperator length] - 4; baseOperator = [jsonOperator substringWithRange: NSMakeRange (0, baseLength)]; } else baseOperator = jsonOperator; if ([sieveOperators containsObject: baseOperator]) { requirement = [operatorRequirements objectForKey: baseOperator]; if (requirement) [requirements addObjectUniquely: requirement]; *operator = baseOperator; } else scriptError = [NSString stringWithFormat: @"Rule has unknown operator '%@'", baseOperator]; } else scriptError = @"Rule without any specified operator"; return (scriptError == nil); } - (BOOL) _validateRuleOperator: (NSString *) operator withFieldType: (UIxFilterFieldType) type { BOOL rc; if (type == UIxFilterFieldTypeSize) rc = [sieveSizeOperators containsObject: operator]; else rc = (![sieveSizeOperators containsObject: operator] && [sieveOperators containsObject: operator]); return rc; } - (BOOL) _extractRuleValue: (NSString **) value fromRule: (NSDictionary *) rule withFieldType: (UIxFilterFieldType) type { NSString *extractedValue; extractedValue = [rule objectForKey: @"value"]; if (extractedValue) { if (type == UIxFilterFieldTypeSize) *value = [NSString stringWithFormat: @"%d", [extractedValue intValue]]; else *value = [extractedValue asSieveQuotedString]; } else scriptError = @"Rule lacks a 'value' parameter"; return (scriptError == nil); } - (NSString *) _composeSieveRuleOnField: (NSString *) field withType: (UIxFilterFieldType) type operator: (NSString *) operator revert: (BOOL) revert andValue: (NSString *) value { NSMutableString *sieveRule; 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 == UIxFilterFieldTypeSize) [sieveRule appendString: @"size "]; [sieveRule appendFormat: @":%@ ", operator]; if (type == UIxFilterFieldTypeSize) [sieveRule appendFormat: @"%@K", value]; else [sieveRule appendFormat: @"%@ %@", field, value]; return sieveRule; } - (NSString *) _extractSieveRule: (NSDictionary *) rule { NSString *field, *operator, *value; UIxFilterFieldType type; BOOL isNot; return (([self _extractRuleField: &field fromRule: rule andType: &type] && [self _extractRuleOperator: &operator fromRule: rule isNot: &isNot] && [self _validateRuleOperator: operator withFieldType: type] && [self _extractRuleValue: &value fromRule: rule withFieldType: type]) ? [self _composeSieveRuleOnField: field withType: type operator: operator revert: isNot andValue: value] : nil); } - (NSArray *) _extractSieveRules: (NSArray *) rules { NSMutableArray *sieveRules; NSString *sieveRule; int count, max; max = [rules count]; if (max) { sieveRules = [NSMutableArray arrayWithCapacity: max]; for (count = 0; !scriptError && count < max; count++) { sieveRule = [self _extractSieveRule: [rules objectAtIndex: count]]; if (sieveRule) [sieveRules addObject: sieveRule]; } } else sieveRules = nil; return sieveRules; } - (NSString *) _extractSieveAction: (NSDictionary *) action { NSString *sieveAction, *method, *requirement, *argument, *flag, *mailbox; SOGoDomainDefaults *dd; sieveAction = nil; method = [action objectForKey: @"method"]; if (method) { argument = [action objectForKey: @"argument"]; if ([method isEqualToString: @"discard"] || [method isEqualToString: @"keep"] || [method isEqualToString: @"stop"]) sieveAction = method; else { if (argument) { if ([method isEqualToString: @"addflag"]) { flag = [sieveFlags objectForKey: argument]; if (flag) sieveAction = [NSString stringWithFormat: @"%@ %@", method, [flag asSieveQuotedString]]; else scriptError = [NSString stringWithFormat: @"Action with invalid flag argument '%@'", argument]; } else if ([method isEqualToString: @"fileinto"]) { dd = [user domainDefaults]; mailbox = [[argument componentsSeparatedByString: @"/"] componentsJoinedByString: [dd imapFolderSeparator]]; sieveAction = [NSString stringWithFormat: @"%@ %@", method, [mailbox asSieveQuotedString]]; } else if ([method isEqualToString: @"redirect"]) sieveAction = [NSString stringWithFormat: @"%@ %@", method, [argument asSieveQuotedString]]; else if ([method isEqualToString: @"reject"]) sieveAction = [NSString stringWithFormat: @"%@ text: %@", method, [argument asSieveQuotedString]]; else scriptError = [NSString stringWithFormat: @"Action has unknown method '%@'", method]; } else scriptError = @"Action missing 'argument' parameter"; } if (method) { requirement = [methodRequirements objectForKey: method]; if (requirement) [requirements addObjectUniquely: requirement]; } } else scriptError = @"Action missing 'method' parameter"; return sieveAction; } - (NSArray *) _extractSieveActions: (NSArray *) actions { NSMutableArray *sieveActions; NSString *sieveAction; int count, max; max = [actions count]; sieveActions = [NSMutableArray arrayWithCapacity: max]; for (count = 0; !scriptError && count < max; count++) { sieveAction = [self _extractSieveAction: [actions objectAtIndex: count]]; if (!scriptError) [sieveActions addObject: sieveAction]; } return sieveActions; } - (NSString *) _convertScriptToSieve: (NSDictionary *) newScript { NSMutableString *sieveText; NSString *match; NSArray *sieveRules, *sieveActions; sieveText = [NSMutableString stringWithCapacity: 1024]; match = [newScript objectForKey: @"match"]; if ([match isEqualToString: @"allmessages"]) match = nil; if (match) { if ([match isEqualToString: @"all"] || [match isEqualToString: @"any"]) { sieveRules = [self _extractSieveRules: [newScript objectForKey: @"rules"]]; if (sieveRules) [sieveText appendFormat: @"if %@of (%@) {\r\n", match, [sieveRules componentsJoinedByString: @", "]]; else scriptError = [NSString stringWithFormat: @"Test '%@' used without any" @" specified rule", match]; } else scriptError = [NSString stringWithFormat: @"Bad test: %@", match]; } sieveActions = [self _extractSieveActions: [newScript objectForKey: @"actions"]]; if ([sieveActions count]) [sieveText appendFormat: @" %@;\r\n", [sieveActions componentsJoinedByString: @";\r\n "]]; if (match) [sieveText appendFormat: @"}\r\n"]; return sieveText; } - (NSString *) sieveScriptWithRequirements: (NSMutableArray *) newRequirements { NSMutableString *sieveScript; NSString *sieveText; NSArray *scripts; int count, max; BOOL previousWasConditional; NSDictionary *currentScript; sieveScript = [NSMutableString stringWithCapacity: 8192]; ASSIGN (requirements, newRequirements); [scriptError release]; scriptError = nil; scripts = [[user userDefaults] sieveFilters]; max = [scripts count]; if (max) { previousWasConditional = NO; for (count = 0; !scriptError && count < max; count++) { currentScript = [scripts objectAtIndex: count]; if ([[currentScript objectForKey: @"active"] boolValue]) { sieveText = [self _convertScriptToSieve: currentScript]; if ([sieveText hasPrefix: @"if"]) { if (previousWasConditional) [sieveScript appendFormat: @"els"]; else previousWasConditional = YES; } else previousWasConditional = NO; [sieveScript appendString: sieveText]; } } } [scriptError retain]; [requirements release]; requirements = nil; if (scriptError) sieveScript = nil; return sieveScript; } - (NSString *) lastScriptError { return scriptError; } @end