Monotone-Parent: 6364fd26aed4532b46cd1c779ef5e9f5f6c0971c

Monotone-Revision: fd35d485688e2370ae5250cfa15931e93abfd567

Monotone-Author: wsourdeau@inverse.ca
Monotone-Date: 2010-03-02T19:57:30
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Wolfgang Sourdeau 2010-03-02 19:57:30 +00:00
parent e2f1faa697
commit f81f48cb90
28 changed files with 2207 additions and 62 deletions

View File

@ -1,5 +1,20 @@
2010-03-02 Wolfgang Sourdeau <wsourdeau@inverse.ca>
* UI/PreferencesUI/UIxPreferences.m (-nameLabel, -colorLabel):
removed useless methods.
* SoObjects/SOGo/SOGoUserDefaults.m (-sieveFilters)
(-setSieveFilters:): new accessors.
* SoObjects/SOGo/SOGoDomainDefaults.m (-sieveScriptEnabled): new
accessor.
* SoObjects/Mailer/SOGoSieveConverter.m: new class for producing
sieve scripts from user defaults.
* UI/PreferencesUI/UIxFilterEditor.m: new template class for
editing sieve scripts.
* UI/WebServerResources/generic.js (_): new gettextable equivalent
of "getLabel".

View File

@ -33,6 +33,8 @@ Mailer_OBJC_FILES += \
SOGoMailForward.m \
SOGoMailReply.m \
\
SOGoSieveConverter.m \
\
EOQualifier+MailDAV.m \
NSData+Mail.m \
NSString+Mail.m

View File

@ -50,8 +50,10 @@
#import "SOGoMailManager.h"
#import "SOGoMailNamespace.h"
#import "SOGoSentFolder.h"
#import "SOGoSieveConverter.h"
#import "SOGoTrashFolder.h"
#import "SOGoMailAccount.h"
@implementation SOGoMailAccount
@ -232,23 +234,25 @@ static NSString *sieveScriptName = @"sogo";
- (BOOL) updateFilters
{
NSMutableString *header, *script;
NSMutableArray *requirements;
NSMutableString *script, *header;
NGInternetSocketAddress *address;
NSDictionary *result, *values;
SOGoUserDefaults *ud;
SOGoDomainDefaults *dd;
NGSieveClient *client;
NSString *v, *password;
NSString *filterScript, *v, *password;
SOGoSieveConverter *converter;
BOOL b;
dd = [[context activeUser] domainDefaults];
if (!([dd vacationEnabled] || [dd forwardEnabled]))
if (!([dd sieveScriptsEnabled] || [dd vacationEnabled] || [dd forwardEnabled]))
return YES;
requirements = [NSMutableArray arrayWithCapacity: 15];
ud = [[context activeUser] userDefaults];
b = NO;
header = [NSMutableString stringWithString: @"require ["];
script = [NSMutableString string];
// Right now, we handle Sieve filters here and only for vacation
@ -274,8 +278,8 @@ static NSString *sieveScriptName = @"sogo";
if (days == 0)
days = 7;
[header appendString: @"\"vacation\""];
[requirements addObjectUniquely: @"vacation"];
// Skip mailing lists
if (ignore)
[script appendString: @"if allof ( not exists [\"list-help\", \"list-unsubscribe\", \"list-subscribe\", \"list-owner\", \"list-post\", \"list-archive\", \"list-id\", \"Mailing-List\"], not header :comparator \"i;ascii-casemap\" :is \"Precedence\" [\"list\", \"bulk\", \"junk\"], not header :comparator \"i;ascii-casemap\" :matches \"To\" \"Multiple recipients of*\" ) {"];
@ -315,9 +319,27 @@ static NSString *sieveScriptName = @"sogo";
[script appendString: @"keep;\r\n"];
}
if ([header compare: @"require ["] != NSOrderedSame)
converter = [SOGoSieveConverter sieveConverterForUser: [context activeUser]];
filterScript = [converter sieveScriptWithRequirements: requirements];
if (filterScript)
{
[header appendString: @"];\r\n"];
if ([filterScript length])
{
b = YES;
[script appendString: filterScript];
}
}
else
{
[self errorWithFormat: @"Sieve generation failure: %@",
[converter lastScriptError]];
return NO;
}
if ([requirements count])
{
header = [NSString stringWithFormat: @"require [\"%@\"];\r\n",
[requirements componentsJoinedByString: @"\",\""]];
[script insertString: header atIndex: 0];
}

View File

@ -0,0 +1,49 @@
/* SOGoSieveConverter.h - this file is part of SOGo
*
* Copyright (C) 2010 Wolfgang Sourdeau
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
*
* 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.
*/
#ifndef SOGOSIEVECONVERTER_H
#define SOGOSIEVECONVERTER_H
#import <Foundation/NSObject.h>
@class NSDictionary;
@class NSMutableArray;
@class NSString;
@class SOGoUser;
@interface SOGoSieveConverter : NSObject
{
SOGoUser *user;
NSMutableArray *requirements;
NSString *scriptError;
}
+ (id) sieveConverterForUser: (SOGoUser *) user;
- (id) initForUser: (SOGoUser *) newUser;
- (NSString *) sieveScriptWithRequirements: (NSMutableArray *) newRequirements;
- (NSString *) lastScriptError;
@end
#endif /* SOGOSIEVECONVERTER_H */

View File

@ -0,0 +1,610 @@
/* SOGoSieveConverter.m - this file is part of SOGo
*
* Copyright (C) 2010 Wolfgang Sourdeau
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
*
* 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 <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+BSJSONAdditions.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoUser.h>
#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;
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";
sieveAction = nil;
}
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

View File

@ -42,6 +42,8 @@
@interface NSMutableDictionary (SOGoDictionaryUtilities)
- (void) setObject: (id) object
forKeys: (NSArray *) keys;
- (void) setObjects: (NSArray *) objects
forKeys: (NSArray *) keys;

View File

@ -184,6 +184,17 @@
@implementation NSMutableDictionary (SOGoDictionaryUtilities)
- (void) setObject: (id) object
forKeys: (NSArray *) keys
{
unsigned int count, max;
max = [keys count];
for (count = 0; count < max; count++)
[self setObject: object
forKey: [keys objectAtIndex: count]];
}
- (void) setObjects: (NSArray *) objects
forKeys: (NSArray *) keys
{

View File

@ -4,6 +4,8 @@
WOLogFile = "/var/log/sogo/sogo.log";
WOPidFile = "/var/run/sogo/sogo.pid";
NGImap4ConnectionStringSeparator = ".";
SOGoZipPath = "/usr/bin/zip";
WOUseRelativeURLs = YES;

View File

@ -43,6 +43,7 @@
- (NSString *) imapFolderSeparator;
- (BOOL) imapAclConformsToIMAPExt;
- (BOOL) forceIMAPLoginWithEmail;
- (BOOL) sieveScriptsEnabled;
- (BOOL) forwardEnabled;
- (BOOL) vacationEnabled;
- (NSString *) mailingMechanism;

View File

@ -155,6 +155,11 @@
return [self boolForKey: @"SOGoForceIMAPLoginWithEmail"];
}
- (BOOL) sieveScriptsEnabled
{
return [self boolForKey: @"SOGoSieveScriptsEnabled"];
}
- (BOOL) forwardEnabled
{
return [self boolForKey: @"SOGoForwardEnabled"];

View File

@ -140,6 +140,9 @@ extern NSString *SOGoWeekStartFirstFullWeek;
- (void) setRemindWithASound: (BOOL) newValue;
- (BOOL) remindWithASound;
- (void) setSieveFilters: (NSArray *) newValue;
- (NSArray *) sieveFilters;
- (void) setVacationOptions: (NSDictionary *) newValue;
- (NSDictionary *) vacationOptions;

View File

@ -565,6 +565,16 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek";
return [self boolForKey: @"SOGoRemindWithASound"];
}
- (void) setSieveFilters: (NSArray *) newValue
{
[self setObject: newValue forKey: @"SOGoSieveFilters"];
}
- (NSArray *) sieveFilters
{
return [self arrayForKey: @"SOGoSieveFilters"];
}
- (void) setVacationOptions: (NSDictionary *) newValue
{
[self setObject: newValue forKey: @"Vacation"];

View File

@ -13,6 +13,8 @@ PreferencesUI_OBJC_FILES = \
\
UIxJSONPreferences.m \
UIxPreferences.m \
UIxFilterEditor.m \
\
UIxAdditionalPreferences.m
PreferencesUI_RESOURCE_FILES += \

View File

@ -0,0 +1,8 @@
# compile settings
SUBMINOR_VERSION:=1
ADDITIONAL_CPPFLAGS += \
-DSOGO_MAJOR_VERSION=$(MAJOR_VERSION) \
-DSOGO_MINOR_VERSION=$(MINOR_VERSION) \
-DSOGO_SUBMINOR_VERSION=$(SUBMINOR_VERSION)

View File

@ -0,0 +1,104 @@
/* UIxFilterEditor.m - this file is part of SOGo
*
* Copyright (C) 2010 Inverse inc.
*
* Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
*
* 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 <Foundation/NSCharacterSet.h>
#import <NGObjWeb/WORequest.h>
#import <NGObjWeb/WOResponse.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoUser.h>
#import <SOGoUI/UIxComponent.h>
@interface UIxFilterEditor : UIxComponent
{
NSString *filterId;
}
@end
@implementation UIxFilterEditor
/* convert and save */
- (BOOL) _validateFilterId
{
NSCharacterSet *digits;
BOOL rc;
digits = [NSCharacterSet decimalDigitCharacterSet];
rc = ([filterId isEqualToString: @"new"]
|| [[filterId stringByTrimmingCharactersInSet: digits] length] == 0);
return rc;
}
- (id <WOActionResults>) defaultAction
{
id <WOActionResults> result;
WORequest *request;
request = [context request];
ASSIGN (filterId, [request formValueForKey: @"filter"]);
if (filterId)
{
if ([self _validateFilterId])
result = self;
else
result = [self responseWithStatus: 403
andString: @"Bad value for 'filter'"];
}
else
result = [self responseWithStatus: 403
andString: @"Missing 'filter' parameter"];
return result;
}
- (NSString *) filterId
{
return filterId;
}
- (NSString *) firstMailAccount
{
NSArray *accounts;
NSDictionary *account;
NSString *login, *accountName;
SOGoUser *ownerUser;
login = [[self clientObject] nameInContainer];
ownerUser = [SOGoUser userWithLogin: login];
accounts = [ownerUser mailAccounts];
if ([accounts count] > 0)
{
account = [accounts objectAtIndex: 0];
accountName = [[account objectForKey: @"name"] asCSSIdentifier];
}
else
accountName = @"";
return accountName;
}
@end

View File

@ -42,6 +42,7 @@
SOGoUserDefaults *userDefaults;
NSCalendarDate *today;
NSArray *daysOfWeek, *daysBetweenResponsesList;
NSArray *sieveFilters;
NSMutableDictionary *vacationOptions, *forwardOptions;
BOOL hasChanged, composeMessageTypeHasChanged;
}

View File

@ -21,10 +21,11 @@
*/
#import <Foundation/NSCalendarDate.h>
#import <Foundation/NSPropertyList.h>
#import <Foundation/NSScanner.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTimeZone.h>
#import <Foundation/NSUserDefaults.h> /* for locale strings */
#import <Foundation/NSPropertyList.h>
#import <Foundation/NSValue.h>
#import <NGObjWeb/WOContext.h>
@ -35,6 +36,7 @@
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+BSJSONAdditions.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSScanner+BSJSONAdditions.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserDefaults.h>
@ -83,6 +85,13 @@
composeMessageTypeHasChanged = NO;
dd = [user domainDefaults];
if ([dd sieveScriptsEnabled])
{
sieveFilters = [[userDefaults sieveFilters] copy];
if (!sieveFilters)
sieveFilters = [NSArray new];
}
if ([dd vacationEnabled])
{
vacationOptions = [[userDefaults vacationOptions] mutableCopy];
@ -107,6 +116,7 @@
[item release];
[user release];
[userDefaults release];
[sieveFilters release];
[vacationOptions release];
[calendarCategories release];
[calendarCategoriesColors release];
@ -661,11 +671,54 @@
/* mail autoreply (vacation) */
- (BOOL) isSieveScriptsEnabled
{
return [[user domainDefaults] sieveScriptsEnabled];
}
- (NSString *) sieveCapabilities
{
#warning this should be deduced from the server
static NSArray *capabilities = nil;
if (!capabilities)
{
capabilities = [NSArray arrayWithObjects: @"fileinto", @"reject",
@"envelope", @"vacation", @"imapflags",
@"notify", @"subaddress", @"relational",
@"comparator-i;ascii-numeric", @"regex", nil];
[capabilities retain];
}
return [[NSDictionary dictionary]
jsonStringForArray: capabilities
withIndentLevel: 0];
}
- (BOOL) isVacationEnabled
{
return [[user domainDefaults] vacationEnabled];
}
- (void) setSieveFiltersValue: (NSString *) newValue
{
NSScanner *jsonScanner;
if ([newValue hasPrefix: @"["])
{
jsonScanner = [NSScanner scannerWithString: newValue];
[jsonScanner scanJSONArray: &sieveFilters];
[sieveFilters retain];
}
}
- (NSString *) sieveFiltersValue
{
return [[NSDictionary dictionary]
jsonStringForArray: sieveFilters
withIndentLevel: 0];
}
- (void) setEnableVacation: (BOOL) enableVacation
{
[vacationOptions setObject: [NSNumber numberWithBool: enableVacation]
@ -884,6 +937,8 @@
id folder;
dd = [[context activeUser] domainDefaults];
if ([dd sieveScriptsEnabled])
[userDefaults setSieveFilters: sieveFilters];
if ([dd vacationEnabled])
[userDefaults setVacationOptions: vacationOptions];
if ([dd forwardEnabled])
@ -897,7 +952,6 @@
account = [folder lookupName: [[mailAccounts objectForKey: @"name"] asCSSIdentifier]
inContext: context
acquire: NO];
[account updateFilters];
if (composeMessageTypeHasChanged)
@ -947,16 +1001,6 @@
userCanChangePassword];
}
- (NSString *) nameLabel
{
return [self labelForKey: @"Name"];
}
- (NSString *) colorLabel
{
return [self labelForKey: @"Color"];
}
- (NSArray *) languageCategories
{
NSArray *categoryLabels;

View File

@ -19,6 +19,10 @@
protectedBy = "View";
pageName = "UIxPreferences";
};
editFilter = {
protectedBy = "View";
pageName = "UIxFilterEditor";
};
identities = {
protectedBy = "View";
pageName = "UIxIdentities";

View File

@ -0,0 +1,64 @@
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE var:component>
<var:component
xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:uix="OGo:uix"
xmlns:rsrc="OGo:url"
xmlns:label="OGo:label"
className="UIxPageFrame"
title="title"
const:popup="YES"
>
<script type="text/javascript">
var filterId = '<var:string value="filterId"/>';
var firstMailAccount = '<var:string value="firstMailAccount"/>';
</script>
<form id="mainForm" var:href="ownPath">
<div id="filterNameContainer" class="container">
<label><var:string label:value="Filter name:"
/><input const:name="filterName" class="textField" type="text"
var:value="filterName"/></label><br/>
</div>
<div id="filterMatchContainer" class="container">
<var:string label:value="For incoming messages that"/>
<select const:name="matchType" const:id="matchType"
><option const:value="all"
><var:string label:value="match all of the following rules:"
/></option
><option const:value="any"
><var:string label:value="match any of the following rules:"
/></option
><option const:value="allmessages"
><var:string label:value="match all messages"
/></option
></select>
</div>
<div id="filterRulesContainer" class="container">
<div id="filterRules"><!-- empty --></div>
<div class="bottomToolbar"
><a const:id="ruleAdd" class="bottomButton" href="#">
<span><img rsrc:src="add-icon.png" label:title="Add" />
</span></a
><a const:id="ruleDelete" class="bottomButton" href="#">
<span><img rsrc:src="remove-icon.png" label:title="Delete" />
</span></a
></div>
</div>
<!-- <div id="splitter" class="dragHandle"></div> -->
<div id="filterActionsContainer" class="container">
<var:string label:value="Perform these actions:"/><br/>
<div id="filterActions"><!-- empty --></div>
<div class="bottomToolbar"
><a const:id="actionAdd" class="bottomButton" href="#">
<span><img rsrc:src="add-icon.png" label:title="Add" />
</span></a
><a const:id="actionDelete" class="bottomButton" href="#">
<span><img rsrc:src="remove-icon.png" label:title="Delete" />
</span></a
></div>
</div>
</form>
</var:component>

View File

@ -101,11 +101,11 @@
><table id="categoriesList" cellspacing="0">
<thead>
<tr class="tableview"
><td const:class="tbtv_headercell" const:id="nameTableHeader"
><var:string var:value="nameLabel" /></td
><td const:class="tbtv_headercell" const:id="colorTableHeader"
><var:string var:value="colorLabel" /></td
></tr
><th const:class="tbtv_headercell" const:id="nameTableHeader"
><var:string label:value="Name"/></th
><th const:class="tbtv_headercell" const:id="colorTableHeader"
><var:string label:value="Color"/></th
></tr
></thead>
<tbody>
<var:foreach list="categoryList" item="category">
@ -173,15 +173,45 @@
const:id="composeMessagesType"
string="itemComposeMessagesText"
selection="userComposeMessagesType"/>
<var:if condition="isSieveScriptsEnabled"
><br/><label><var:string label:value="Filters"/></label>
<script type="text/javascript">
var sieveCapabilities = <var:string value="sieveCapabilities" const:escapeHTML="NO"/>;
</script>
<div id="filtersListWrapper"
><table id="filtersList" cellspacing="0">
<thead>
<tr class="tableview"
><th const:class="tbtv_headercell" const:id="nameTableHeader"
><var:string label:value="Name" /></th
><th const:class="tbtv_headercell" const:id="activeTableHeader"
><var:string label:value="Active" /></th
></tr
></thead>
<tbody><!--space --></tbody>
</table>
<input type="hidden" const:name="sieveFilters" const:id="sieveFilters"
var:value="sieveFiltersValue"/></div>
<div class="bottomToolbar">
<a const:id="filterAdd" class="bottomButton" href="#">
<span><img rsrc:src="add-icon.png" label:title="Add" />
</span></a>
<a const:id="filterDelete" class="bottomButton" href="#">
<span><img rsrc:src="remove-icon.png" label:title="Delete" />
</span></a>
<a const:id="filterMoveUp" class="bottomButton" href="#">
<span><img rsrc:src="up-icon.png" label:title="Move Up" />
</span></a>
<a const:id="filterMoveDown" class="bottomButton" href="#">
<span><img rsrc:src="down-icon.png" label:title="Move Down" />
</span></a>
</div>
</var:if>
</div>
<div id="identitiesView" class="tab">
<!--<var:multiselection const:id="identitiesList" item="item"
list="identitiesList" displayString="itemIdentityText">
</var:multiselection>
<br/>-->
<textarea const:id="signature" const:name="signature"
var:value="signature" />
</div>
<div id="identitiesView" class="tab"
><textarea const:id="signature" const:name="signature"
var:value="signature"
/></div>
<var:if condition="isVacationEnabled">
<div id="vacationView" class="tab">
<label><input type="checkbox"

View File

@ -101,14 +101,6 @@ function validateContactEditor() {
return rc;
}
function showCoords(node) {
node = $("givenName");
window.alert("x: " + node.cascadeLeftOffset()
+ ";y: " + node.cascadeTopOffset()
+ ";width: " + window.width()
+ ";height: " + window.height());
}
function onFnKeyDown() {
var fn = $("fn");
fn.onkeydown = null;

View File

@ -0,0 +1,103 @@
DIV#pageContent
{ padding: 10px;
padding-top: 15px; }
#splitter
{ cursor: n-resize;
top: 230px;
left: 0px;
right: 0px;
height: 5px; }
LABEL
{ margin: 0px;
padding: 0px; }
#pageContent INPUT,
#pageContent SELECT
{ background-color: #ccddec;
padding: 1px;
margin: 1px;
border-width: 1px; }
#pageContent INPUT
{ padding-left: 2px; }
DIV.container
{ margin-bottom: 14px; }
DIV#filterRulesContainer,
DIV#filterActionsContainer
{ position: absolute;
margin: 0px;
border: 0px;
padding: 0px;
left: 10px;
right: 10px; }
DIV#filterRulesContainer
{ top: 128px;
height: 102px; }
DIV#filterActionsContainer
{ top: 235px;
bottom: 0px; }
DIV#filterRules, DIV#filterActions
{ position: absolute;
cursor: default;
left: 0px;
right: 0px;
bottom: 21px;
border-top: 1px solid #909090;
border-left: 1px solid #909090;
border-bottom: 1px solid #ffffff;
border-right: 1px solid #ffffff;
background: #ccddec;
overflow: hidden;
overflow-y: auto; }
DIV#filterRules
{ top: 0px; }
DIV#filterActions
{ top: 20px; }
DIV.bottomToolbar
{ position: absolute;
left: 0px;
right: 0px;
margin: 0px;
padding: 0px;
bottom: 0px; }
DIV.rule,
DIV.action
{ overflow: hidden;
padding-left: 2px;
padding-right: 2px;
width: 100%;
border-bottom: 1px solid #9b9b9b; }
DIV.rule._selected,
DIV.action._selected
{ background-color: #9abcd8;
color: #fff; }
SPAN.fieldContainer
{ width: 100px; }
SPAN.operatorContainer
{ width: 300px; }
SPAN.valueContainer INPUT
{ width: 160px; }
SPAN.fileintoArgument SELECT
{ max-width: 250px; }
SPAN.rejectArgument TEXTAREA
{ height: 54px;
width: 90%;
margin-left: 5%;
padding-right: -50px; }

View File

@ -0,0 +1,794 @@
/* -*- Mode: java; tab-width: 2; c-label-minimum-indentation: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* Cyrus: comparator-i;ascii-numeric fileinto reject vacation imapflags
notify envelope relational regex subaddress copy */
var sieveCapabilities = [];
var filter;
var selectedRuleDiv = null;
var selectedActionDiv = null;
var fieldLabels;
var methodLabels;
var operatorLabels;
var operatorRequirements;
var methodRequirements;
var flagLabels;
var mailboxes = [];
function onLoadHandler() {
setupConstants();
setupEventHandlers();
if (window.opener)
sieveCapabilities = window.opener.getSieveCapabilitiesFromEditor();
if (!window.opener || filterId == "new") {
setupNewFilterData();
} else {
filter = window.opener.getFilterFromEditor(filterId);
}
if (!window.opener || window.opener.userMailboxes) {
setupFilterViews();
} else {
loadMailboxes();
}
}
function loadMailboxes() {
var url = (ApplicationBaseURL + "Mail/"
+ encodeURI(firstMailAccount) + "/mailboxes");
triggerAjaxRequest(url, onLoadMailboxesCallback);
}
function onLoadMailboxesCallback(http) {
if (http.readyState == 4) {
window.opener.userMailboxes = [];
if (http.status == 200) {
checkAjaxRequestsState();
if (http.responseText.length > 0) {
var jsonResponse = http.responseText.evalJSON(true);
var responseMboxes = jsonResponse.mailboxes;
for (var i = 0; i < responseMboxes.length; i++) {
var name = responseMboxes[i].path.substr(1);
window.opener.userMailboxes.push(name);
}
}
}
setupFilterViews();
}
}
function setupConstants() {
fieldLabels = { "subject": _("Subject"),
"from": _("From"),
"to": _("To"),
"cc": _("Cc"),
"to_or_cc": _("To or Cc"),
"size": _("Size (Kb)"),
"header": _("Header") };
methodLabels = { "addflag": _("Flag the message with:"),
"discard": _("Discard the message"),
"fileinto": _("File the message in:"),
"keep": _("Keep the message"),
"redirect": _("Forward the message to:"),
"reject": _("Send a reject message:"),
"vacation": _("Send a vacation message"),
"stop": _("Stop processing filter rules") };
operatorLabels = { "under": _("is under"),
"over": _("is over"),
"is": _("is"),
"is_not": _("is not"),
"contains": _("contains"),
"contains_not": _("does not contain"),
"matches": _("matches"),
"matches_not": _("does not match"),
"regex": _("matches regex"),
"regex_not": _("does not match regex") };
flagLabels = { "seen": _("Seen"),
"deleted": _("Deleted"),
"answered": _("Answered"),
"flagged": _("Flagged"),
"junk": _("Junk"),
"not_junk": _("Not Junk") };
for (var i = 1; i < 6; i++) {
var key = "label" + i;
flagLabels[key] = _("Label " + i);
}
}
function setupEventHandlers() {
var filterName = $("mainForm").filterName;
if (filterName) {
var boundCB = onFilterNameChange
.bindAsEventListener(filterName);
filterName.observe("change", boundCB);
}
var matchTypeSelect = $("matchType");
if (matchTypeSelect) {
var boundCB = onMatchTypeChange
.bindAsEventListener(matchTypeSelect);
matchTypeSelect.observe("change", boundCB);
}
var filterRules = $("filterRules");
var boundCB = onFilterRulesDivClick
.bindAsEventListener(filterRules);
filterRules.observe("click", boundCB);
var ruleAdd = $("ruleAdd");
if (ruleAdd) {
var boundCB = onRuleAddClick.bindAsEventListener(ruleAdd);
ruleAdd.observe("click", boundCB);
}
var ruleDelete = $("ruleDelete");
if (ruleDelete) {
var boundCB = onRuleDeleteClick.bindAsEventListener(ruleDelete);
ruleDelete.observe("click", boundCB);
}
var filterActions = $("filterActions");
var boundCB = onFilterActionsDivClick
.bindAsEventListener(filterActions);
filterActions.observe("click", boundCB);
var actionAdd = $("actionAdd");
if (actionAdd) {
var boundCB = onActionAddClick.bindAsEventListener(actionAdd);
actionAdd.observe("click", boundCB);
}
var actionDelete = $("actionDelete");
if (actionDelete) {
var boundCB = onActionDeleteClick
.bindAsEventListener(actionDelete);
actionDelete.observe("click", boundCB);
}
}
function onFilterNameChange(event) {
filter.name = this.value;
}
function onMatchTypeChange() {
var matchType = this.value;
filter.match = matchType;
var container = $("filterRulesContainer");
var otherContainer = $("filterActionsContainer");
var otherContainerTop;
if (matchType == "allmessages") {
container.hide();
otherContainerTop = 130;
} else {
container.show();
otherContainerTop = 240;
}
otherContainer.setStyle({ top: otherContainerTop + "px" });
}
function onFilterRulesDivClick(event) {
setSelectedRuleDiv(null);
event.stop();
}
function onFilterActionsDivClick(event) {
setSelectedActionDiv(null);
event.stop();
}
function createFilterRule() {
return { field: "subject", operator: "contains", value: "" };
}
function createFilterAction() {
return { method: "fileinto", argument: "INBOX" };
}
function setupNewFilterData() {
var newFilterTemplate = $({ name: _("Untitled Filter"),
match: "any" });
newFilterTemplate.rules = $([ createFilterRule() ]);
newFilterTemplate.actions = $([ createFilterAction() ]);
filter = newFilterTemplate;
}
function setupFilterViews() {
var filterName = $("mainForm").filterName;
if (filterName) {
filterName.value = filter.name;
if (filterId == "new") {
filterName.focus();
$(filterName).selectText(0, filterName.value.length);
}
}
var matchTypeSelect = $("matchType");
if (matchTypeSelect) {
matchTypeSelect.value = filter.match;
}
if (filter.match != "allmessages") {
var filterRules = $("filterRules");
if (filterRules && filter.rules) {
for (var i = 0; i < filter.rules.length; i++) {
appendRule(filterRules, filter.rules[i]);
}
}
}
var filterActions = $("filterActions");
if (filterActions && filter.actions) {
for (var i = 0; i < filter.actions.length; i++) {
appendAction(filterActions, filter.actions[i]);
}
}
}
function appendRule(container, rule) {
var ruleDiv = createElement("div", null, "rule",
{ rule: rule }, null,
container);
var boundCB = onRuleDivClick.bindAsEventListener(ruleDiv);
ruleDiv.observe("click", boundCB);
ensureRuleRepresentation(ruleDiv);
return ruleDiv;
}
function onRuleDivClick(event) {
setSelectedRuleDiv(this);
event.stop();
}
function setSelectedRuleDiv(newDiv) {
if (selectedRuleDiv) {
selectedRuleDiv.removeClassName("_selected");
}
selectedRuleDiv = newDiv;
if (selectedRuleDiv) {
selectedRuleDiv.addClassName("_selected");
}
}
function ensureRuleRepresentation(container) {
ensureFieldRepresentation(container);
ensureOperatorRepresentation(container);
ensureValueRepresentation(container);
}
function ensureFieldRepresentation(container) {
var fieldSpans = container.select("SPAN.fieldContainer");
var fieldSpan;
if (fieldSpans.length)
fieldSpan = fieldSpans[0];
else {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
fieldSpan = createElement("span", null, "fieldContainer",
null, null, container);
}
ensureFieldSelectRepresentation(container, fieldSpan);
ensureFieldCustomHeaderRepresentation(container, fieldSpan);
}
function ensureFieldSelectRepresentation(container, fieldSpan) {
var fields
= [ "subject", "from", "to", "cc", "to_or_cc", "size", "header" ];
var selects = fieldSpan.select("SELECT");
var select;
if (selects.length)
select = selects[0];
else {
select = createElement("select");
select.rule = container.rule;
var boundCB = onFieldSelectChange.bindAsEventListener(select);
select.observe("change", boundCB);
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var fieldOption = createElement("option", null, null,
{ value: field }, null, select);
fieldOption.appendChild(document
.createTextNode(fieldLabels[field]));
}
fieldSpan.appendChild(select);
}
select.value = container.rule.field;
container.rule.field = select.value;
}
function onFieldSelectChange(event) {
this.rule.field = this.value;
var fieldSpan = this.parentNode;
var container = fieldSpan.parentNode;
ensureFieldCustomHeaderRepresentation(container, fieldSpan);
ensureOperatorRepresentation(container);
ensureValueRepresentation(container);
}
function ensureFieldCustomHeaderRepresentation(container, fieldSpan) {
var headerInputs = fieldSpan.select("INPUT");
var headerInput = null;
if (headerInputs.length) {
headerInput = headerInputs[0];
}
if (container.rule.field == "header") {
if (!headerInput) {
headerInput = createElement("input", null, "textField",
{ type: "text" }, null, fieldSpan);
headerInput.rule = container.rule;
if (!container.rule.custom_header)
container.rule.custom_header = "";
headerInput.value = container.rule.custom_header;
var boundCB
= onFieldCustomHeaderChange.bindAsEventListener(headerInput);
headerInput.observe("change", boundCB);
headerInput.focus();
}
} else {
if (headerInput) {
if (container.rule.custom_header)
container.rule.custom_header = null;
fieldSpan.removeChild(headerInput);
}
}
}
function onFieldCustomHeaderChange(event) {
this.rule.custom_header = this.value;
}
function ensureOperatorRepresentation(container) {
var operatorSpans = container.select("SPAN.operatorContainer");
var operatorSpan;
if (operatorSpans.length)
operatorSpan = operatorSpans[0];
else
operatorSpan = createElement("span", null, "operatorContainer",
null, null, container);
ensureOperatorSelectRepresentation(container, operatorSpan);
}
function ensureOperatorSelectRepresentation(container, operatorSpan) {
var operators = determineOperators(container.rule.field);
var ruleField = container.rule.field;
var selects = operatorSpan.select("SELECT");
var select = null;
if (selects.length) {
select = selects[0];
if ((ruleField == "size" && !select.sizeOperator)
|| (ruleField != "size" && select.sizeOperator)) {
operatorSpan.removeChild(select);
select = null;
}
}
if (!select) {
select = createElement("select");
select.rule = container.rule;
select.sizeOperator = (ruleField == "size");
var boundCB = onOperatorSelectChange.bindAsEventListener(select);
select.observe("change", boundCB);
for (var i = 0; i < operators.length; i++) {
var operator = operators[i];
var operatorOption = createElement("option", null, null,
{ value: operator }, null,
select);
operatorOption.appendChild(document
.createTextNode(operatorLabels[operator]));
}
operatorSpan.appendChild(select);
}
if (container.rule.operator
&& operators.indexOf(container.rule.operator) == -1) {
container.rule.operator = operators[0];
}
select.value = container.rule.operator;
container.rule.operator = select.value;
}
function onOperatorSelectChange(event) {
this.rule.operator = this.value;
var valueSpans = this.parentNode.parentNode.select("SPAN.valueContainer");
if (valueSpans.length) {
var valueInputs = valueSpans[0].select("INPUT");
if (valueInputs.length) {
valueInputs[0].focus();
}
}
}
function determineOperators(field) {
var operators;
if (field == "size") {
operators = [ "under", "over" ];
} else {
var baseOperators = [ "is", "contains", "matches" ];
if (sieveCapabilities.indexOf("regex") > -1) {
baseOperators.push("regex");
}
operators = [];
for (var i = 0; i < baseOperators.length; i++) {
operators.push(baseOperators[i]);
operators.push(baseOperators[i] + "_not");
}
}
return operators;
}
function ensureValueRepresentation(container) {
var valueSpans = container.select("SPAN.valueContainer");
var valueSpan;
if (valueSpans.length)
valueSpan = valueSpans[0];
else
valueSpan = createElement("span", null, "valueContainer",
null, null, container);
ensureValueInputRepresentation(container, valueSpan);
}
function ensureValueInputRepresentation(container, valueSpan) {
var inputs = valueSpan.select("INPUT");
var input;
if (inputs.length) {
input = inputs[0];
}
else {
input = createElement("input", null, "textField");
input.rule = container.rule;
var boundCB = onValueInputChange.bindAsEventListener(input);
input.observe("change", boundCB);
valueSpan.appendChild(input);
}
input.value = container.rule.value;
ensureFieldValidity(input);
}
function ensureFieldValidity(input) {
var valid = true;
if (input.rule.field == "size") {
valid = ensureFieldIsNumerical(input);
} else
input.removeClassName("_invalid");
return valid;
}
function onValueInputChange(event) {
if (ensureFieldValidity(this))
this.rule.value = this.value;
else
this.rule.value = "0";
}
function ensureFieldIsNumerical(input) {
var valid = !isNaN(input.value);
if (valid) {
input.removeClassName("_invalid");
} else {
input.addClassName("_invalid");
}
return valid;
}
function appendAction(container, action) {
var actionDiv = createElement("div", null, "action",
{ action: action }, null,
container);
var boundCB = onActionDivClick.bindAsEventListener(actionDiv);
actionDiv.observe("click", boundCB);
ensureActionRepresentation(actionDiv);
return actionDiv;
}
function onActionDivClick(event) {
setSelectedActionDiv(this);
event.stop();
}
function setSelectedActionDiv(newSpan) {
if (selectedActionDiv) {
selectedActionDiv.removeClassName("_selected");
}
selectedActionDiv = newSpan;
if (selectedActionDiv) {
selectedActionDiv.addClassName("_selected");
}
}
function ensureActionRepresentation(container) {
ensureMethodRepresentation(container);
ensureArgumentRepresentation(container);
}
function ensureMethodRepresentation(container) {
var methodSpans = container.select("SPAN.methodContainer");
var methodSpan;
if (methodSpans.length)
methodSpan = methodSpans[0];
else {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
methodSpan = createElement("span", null, "methodContainer",
null, null, container);
}
ensureMethodSelectRepresentation(container, methodSpan);
}
function ensureMethodSelectRepresentation(container, methodSpan) {
var methods = [ "redirect", "discard", "keep" ];
if (sieveCapabilities.indexOf("reject") > -1) {
methods.push("reject");
}
if (sieveCapabilities.indexOf("fileinto") > -1) {
methods.push("fileinto");
}
if (sieveCapabilities.indexOf("imapflags") > -1) {
methods.push("addflag");
}
methods.push("stop");
/* TODO: those are currently unimplemented */
// if (sieveCapabilities.indexOf("notify") > -1) {
// methods.push("notify");
// }
// if (sieveCapabilities.indexOf("vacation") > -1) {
// methods.push("vacation");
// }
var selects = methodSpan.select("SELECT");
var select;
if (selects.length)
select = selects[0];
else {
select = createElement("select");
select.action = container.action;
var boundCB = onMethodSelectChange.bindAsEventListener(select);
select.observe("change", boundCB);
for (var i = 0; i < methods.length; i++) {
var method = methods[i];
var methodOption = createElement("option", null, null,
{ value: method }, null, select);
methodOption.appendChild(document
.createTextNode(methodLabels[method]));
}
methodSpan.appendChild(select);
}
select.value = container.action.method;
}
function onMethodSelectChange(event) {
this.action.method = this.value;
var methodSpan = this.parentNode;
var container = methodSpan.parentNode;
ensureArgumentRepresentation(container);
}
function ensureArgumentRepresentation(container) {
var argumentWidgetMethods
= { "addflag": ensureFlagArgRepresentation,
"fileinto": ensureMailboxArgRepresentation,
"redirect": ensureRedirectArgRepresentation,
"reject": ensureRejectArgRepresentation,
"vacation": ensureVacationArgRepresentation };
var widgetMethod = argumentWidgetMethods[container.action.method];
var spanClass = container.action.method + "Argument";
var argumentSpans = container.select("SPAN.argumentContainer");
var argumentSpan;
if (argumentSpans.length) {
argumentSpan = argumentSpans[0];
if (argumentSpan
&& (!widgetMethod || !argumentSpan.hasClassName(spanClass))) {
container.removeChild(argumentSpan);
container.action.argument = null;
argumentSpan = null;
}
}
else
argumentSpan = null;
if (!argumentSpan && widgetMethod) {
argumentSpan = createElement("span", null,
["argumentContainer", spanClass],
null, null, container);
widgetMethod(container, argumentSpan);
}
}
function ensureFlagArgRepresentation(container, argumentSpan) {
var flags = [ "seen", "deleted", "answered", "flagged", "junk",
"not_junk" ];
for (var i = 1; i < 6; i++) {
flags.push("label" + i);
}
var selects = argumentSpan.select("SELECT");
var select;
if (selects.length)
select = selects[0];
else {
select = createElement("select");
select.action = container.action;
var boundCB = onFlagArgumentSelectChange.bindAsEventListener(select);
select.observe("change", boundCB);
for (var i = 0; i < flags.length; i++) {
var flag = flags[i];
var flagOption = createElement("option", null, null,
{ value: flag }, null, select);
var label = flagLabels[flag];
flagOption.appendChild(document.createTextNode(label));
}
argumentSpan.appendChild(select);
}
/* 1) initialize the value if null
2) set the SELECT to the corresponding value
3) if value was not null in 1, we must ensure the SELECT contains it */
if (!container.action.argument)
container.action.argument = "seen";
select.value = container.action.argument;
container.action.argument = select.value;
}
function onFlagArgumentSelectChange(event) {
this.action.argument = this.value;
}
function ensureMailboxArgRepresentation(container, argumentSpan) {
var selects = argumentSpan.select("SELECT");
var select;
if (selects.length)
select = selects[0];
else {
select = createElement("select");
select.action = container.action;
if (!container.action.argument)
container.action.argument = "INBOX";
var boundCB = onMailboxArgumentSelectChange.bindAsEventListener(select);
select.observe("change", boundCB);
var mailboxes = (window.opener
? window.opener.userMailboxes
: ["INBOX" ]);
for (var i = 0; i < mailboxes.length; i++) {
var mailbox = mailboxes[i];
var mboxOption = createElement("option", null, null,
{ value: mailbox }, null, select);
mboxOption.appendChild(document.createTextNode(mailbox));
}
argumentSpan.appendChild(select);
}
select.value = container.action.argument;
container.action.argument = select.value;
}
function onMailboxArgumentSelectChange(event) {
this.action.argument = this.value;
}
function ensureRedirectArgRepresentation(container, argumentSpan) {
var emailInputs = argumentSpan.select("INPUT");
var emailInput = null;
if (emailInputs.length) {
emailInput = emailInputs[0];
}
if (!emailInput) {
emailInput = createElement("input", null, "textField",
{ type: "text" }, null, argumentSpan);
emailInput.action = container.action;
if (!container.action.argument)
container.action.argument = "";
var boundCB
= onEmailArgumentChange.bindAsEventListener(emailInput);
emailInput.observe("change", boundCB);
emailInput.focus();
}
emailInput.value = container.action.argument;
}
function onEmailArgumentChange(event) {
this.action.argument = this.value;
}
function ensureRejectArgRepresentation(container, argumentSpan) {
var msgAreas = argumentSpan.select("TEXTAREA");
var msgArea = null;
if (msgAreas.length) {
msgArea = msgAreas[0];
}
if (!msgArea) {
msgArea = createElement("textarea", null, null,
{ action: container.action }, null,
argumentSpan);
if (!container.action.argument)
container.action.argument = "";
var boundCB
= onMsgArgumentChange.bindAsEventListener(msgArea);
msgArea.observe("change", boundCB);
msgArea.focus();
}
msgArea.value = container.action.argument;
}
function onMsgArgumentChange(event) {
this.action.argument = this.value;
}
function ensureVacationArgRepresentation(container, argumentSpan) {
}
function onRuleAddClick(event) {
var filterRules = $("filterRules");
if (filterRules) {
var newRule = createFilterRule();
filter.rules.push(newRule);
var newRuleDiv = appendRule(filterRules, newRule);
setSelectedRuleDiv(newRuleDiv);
filterRules.scrollTop = newRuleDiv.offsetTop;
}
event.stop();
}
function onRuleDeleteClick(event) {
if (selectedRuleDiv) {
var ruleIndex = filter.rules.indexOf(selectedRuleDiv.rule);
filter.rules.splice(ruleIndex, 1);
var nextSelected = selectedRuleDiv.next();
if (!nextSelected)
nextSelected = selectedRuleDiv.previous();
selectedRuleDiv.parentNode.removeChild(selectedRuleDiv);
setSelectedRuleDiv(nextSelected);
}
event.stop();
}
function onActionAddClick(event) {
var filterActions = $("filterActions");
if (filterActions) {
var newAction = createFilterAction();
filter.actions.push(newAction);
var newActionDiv = appendAction(filterActions, newAction);
setSelectedActionDiv(newActionDiv);
filterActions.scrollTop = newActionDiv.offsetTop;
}
event.stop();
}
function onActionDeleteClick(event) {
if (selectedActionDiv) {
var actionIndex = filter.actions.indexOf(selectedActionDiv.action);
filter.actions.splice(actionIndex, 1);
var nextSelected = selectedActionDiv.next();
if (!nextSelected)
nextSelected = selectedActionDiv.previous();
selectedActionDiv.parentNode.removeChild(selectedActionDiv);
setSelectedActionDiv(nextSelected);
}
event.stop();
}
function savePreferences() {
if (window.opener) {
window.opener.updateFilterFromEditor(filterId, filter);
}
window.close();
return false;
}
// function configureDragHandles() {
// var handle = $("splitter");
// if (handle) {
// handle.addInterface(SOGoDragHandlesInterface);
// handle.upperBlock = $("filterRulesContainer");
// handle.lowerBlock = $("filterActionsContainer");
// }
// }
document.observe("dom:loaded", onLoadHandler);

View File

@ -76,3 +76,33 @@ DIV.bottomToolbar
left: 2em;
right: 2em;
margin: 0px; }
/* mail filters */
DIV#filtersListWrapper
{ overflow: auto;
position: absolute;
bottom: 30px;
right: 2em;
top: 154px;
left: 2em;
padding: 0px;
margin-top: 2px;
border-left: 1px solid #9b9b9b;
background: #ccddec; }
TABLE#filtersList
{ width: 100%; }
TR.filterListRow
{ background: #ccddec;
cursor: pointer; }
TD.filterListCell
{ -moz-user-select: none; }
TH#activeTableHeader
{ width: 50px;
text-align: center; }
TD.activeColumn
{ text-align: center; }

View File

@ -1,4 +1,7 @@
/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* -*- Mode: java; tab-width: 2; c-label-minimum-indentation: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
var isSieveScriptsEnabled = false;
var filters = [];
function savePreferences(sender) {
var sendForm = true;
@ -37,12 +40,41 @@ function savePreferences(sender) {
}
}
if (isSieveScriptsEnabled) {
var jsonFilters = prototypeIfyFilters();
$("sieveFilters").setValue(jsonFilters.toJSON());
}
if (sendForm)
$("mainForm").submit();
$("mainForm").submit();
return false;
}
function prototypeIfyFilters() {
var newFilters = $([]);
for (var i = 0; i < filters.length; i++) {
var filter = filters[i];
var newFilter = $({});
newFilter.name = filter.name;
newFilter.match = filter.match;
newFilter.active = filter.active;
newFilter.rules = $([]);
for (var j = 0; j < filter.rules.length; j++) {
newFilter.rules.push($(filter.rules[j]));
}
newFilter.actions = $([]);
for (var j = 0; j < filter.actions.length; j++) {
newFilter.actions.push($(filter.actions[j]));
}
newFilters.push(newFilter);
}
return newFilters;
}
function _setupEvents(enable) {
var widgets = [ "timezone", "shortDateFormat", "longDateFormat",
"timeFormat", "weekStartDay", "dayStartTime", "dayEndTime",
@ -92,6 +124,10 @@ function addDefaultEmailAddresses() {
}
function initPreferences() {
var filtersListWrapper = $("filtersListWrapper");
if (filtersListWrapper) {
isSieveScriptsEnabled = true;
}
_setupEvents(true);
if (typeof (initAdditionalPreferences) != "undefined")
initAdditionalPreferences();
@ -122,7 +158,205 @@ function initPreferences() {
}
if ($("addDefaultEmailAddresses"))
$("addDefaultEmailAddresses").observe("click", addDefaultEmailAddresses);
$("addDefaultEmailAddresses").observe("click",
addDefaultEmailAddresses);
initSieveFilters();
}
function initSieveFilters() {
var table = $("filtersList");
if (table) {
var filtersValue = $("sieveFilters").getValue();
if (filtersValue && filtersValue.length) {
filters = $(filtersValue.evalJSON(false));
for (var i = 0; i < filters.length; i++) {
appendSieveFilterRow(table, i, filters[i]);
}
}
$("filterAdd").observe("click", onFilterAdd);
$("filterDelete").observe("click", onFilterDelete);
$("filterMoveUp").observe("click", onFilterMoveUp);
$("filterMoveDown").observe("click", onFilterMoveDown);
}
}
function appendSieveFilterRow(filterTable, number, filter) {
var row = createElement("tr");
row.observe("mousedown", onRowClick);
row.observe("dblclick", onFilterEdit.bindAsEventListener(row));
var nameColumn = createElement("td");
nameColumn.appendChild(document.createTextNode(filter["name"]));
row.appendChild(nameColumn);
var activeColumn = createElement("td", null, "activeColumn");
var cb = createElement("input", null, "checkBox",
{ checked: filter.active,
type: "checkbox" },
null, activeColumn);
var bound = onScriptActiveCheck.bindAsEventListener(cb);
cb.observe("change", bound);
row.appendChild(activeColumn);
filterTable.tBodies[0].appendChild(row);
}
function onScriptActiveCheck(event) {
var index = this.parentNode.parentNode.rowIndex - 1;
filters[index].active = this.checked;
}
function updateSieveFilterRow(filterTable, number, filter) {
var row = $(filterTable.tBodies[0].rows[number]);
var columns = row.childNodesWithTag("td");
var nameColumn = columns[0];
while (nameColumn.firstChild) {
nameColumn.removeChild(nameColumn.firstChild);
}
nameColumn.appendChild(document.createTextNode(filter.name));
var activeColumn = columns[1];
while (activeColumn.firstChild) {
activeColumn.removeChild(activeColumn.firstChild);
}
createElement("input", null, "checkBox",
{ checked: filter.active,
type: "checkbox" },
null, activeColumn);
}
function _editFilter(filterId) {
var urlstr = ApplicationBaseURL + "editFilter?filter=" + filterId;
var win = window.open(urlstr, "sieve_filter_" + filterId,
"width=450,height=380,resizable=0");
if (win)
win.focus();
}
function onFilterAdd(event) {
log("onFilterAdd");
_editFilter("new");
event.stop();
}
function onFilterDelete(event) {
var filtersList = $("filtersList").tBodies[0];
var nodes = filtersList.getSelectedNodes();
if (nodes.length > 0) {
var deletedFilters = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
deletedFilters.push(node.rowIndex - 1);
}
deletedFilters = deletedFilters.sort(function (x,y) { return x-y; });
var rows = filtersList.rows;
for (var i = 0; i < deletedFilters.length; i++) {
var filterNbr = deletedFilters[i];
filters.splice(filterNbr, 1);
var row = rows[filterNbr];
row.parentNode.removeChild(row);
}
}
event.stop();
}
function onFilterMoveUp(event) {
var filtersList = $("filtersList").tBodies[0];
var nodes = filtersList.getSelectedNodes();
if (nodes.length > 0) {
var node = nodes[0];
var previous = node.previous();
if (previous) {
var count = node.rowIndex - 1;
node.parentNode.removeChild(node);
filtersList.insertBefore(node, previous);
var swapFilter = filters[count];
filters[count] = filters[count - 1];
filters[count - 1] = swapFilter;
}
}
event.stop();
}
function onFilterMoveDown(event) {
var filtersList = $("filtersList").tBodies[0];
var nodes = filtersList.getSelectedNodes();
if (nodes.length > 0) {
var node = nodes[0];
var next = node.next();
if (next) {
var count = node.rowIndex - 1;
filtersList.removeChild(next);
filtersList.insertBefore(next, node);
var swapFilter = filters[count];
filters[count] = filters[count + 1];
filters[count + 1] = swapFilter;
}
}
event.stop();
}
function onFilterEdit(event) {
_editFilter(this.rowIndex - 1);
event.stop();
}
function copyFilter(originalFilter) {
var newFilter = {};
newFilter.name = originalFilter.name;
newFilter.match = originalFilter.match;
newFilter.active = originalFilter.active;
newFilter.rules = [];
for (var i = 0; i < originalFilter.rules.length; i++) {
newFilter.rules.push(_copyFilterElement(originalFilter.rules[i]));
}
newFilter.actions = [];
for (var i = 0; i < originalFilter.actions.length; i++) {
newFilter.actions.push(_copyFilterElement(originalFilter.actions[i]));
}
return newFilter;
}
function _copyFilterElement(filterElement) { /* element = rule or action */
var newElement = {};
for (var k in filterElement) {
var value = filterElement[k];
if (typeof(value) == "string" || typeof(value) == "number") {
newElement[k] = value;
}
}
return newElement;
}
function getSieveCapabilitiesFromEditor() {
return sieveCapabilities;
}
function getFilterFromEditor(filterId) {
return copyFilter(filters[filterId]);
}
function updateFilterFromEditor(filterId, filter) {
var sanitized = {};
for (var k in filter) {
if (!(k == "rules" && filter.match == "allmessages")) {
sanitized[k] = filter[k];
}
}
var table = $("filtersList");
if (filterId == "new") {
var newNumber = filters.length;
filters.push(sanitized);
appendSieveFilterRow(table, newNumber, sanitized);
} else {
filters[filterId] = sanitized;
updateSieveFilterRow(table, filterId, sanitized);
}
}
function resetTableActions() {
@ -292,7 +526,7 @@ function onComposeMessagesTypeChange(event) {
return savePreferences();
else {
// Restore previous value of composeMessagesType
$("composeMessagesType").stopObserving("change", onComposeMessagesTypeChange);
$("composeMessagesType").stopObserving("change", onComposeMessagesTypeChange);
$("composeMessagesType").value = ((Event.element(event).value == 1)?"0":"1");
Event.element(event).blur();
$("composeMessagesType").observe("change", onComposeMessagesTypeChange);
@ -304,13 +538,13 @@ function onComposeMessagesTypeChange(event) {
// HTML mode
CKEDITOR.replace('signature',
{
height: "290px",
toolbar :
height: "290px",
toolbar :
[['Bold', 'Italic', '-', 'Link',
'Font','FontSize','-','TextColor',
'BGColor']
]
}
}
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

View File

@ -90,7 +90,7 @@ a:hover
div#header
{ margin-left: 5px;
margin-right: 5px;
padding: 0;
padding: 0px;
border-bottom: 1px solid #000; }
div#header img.headerlogo
@ -540,8 +540,8 @@ DIV.resize-handle
width: 2px;
border-right: 1px solid #fff;
position: absolute;
top: 0;
left: 0;
top: 0px;
left: 0px;
max-height: 2em; } /* will be set in JavaScript when setting drag handles */
@media print
@ -584,7 +584,7 @@ DIV.dialog.left
background-position: top left; }
DIV.dialog.left DIV
{ border-left: 0;
{ border-left: 0px;
margin-left: 19px;
text-align: left; }
@ -594,7 +594,7 @@ DIV.dialog.right
background-position: top right; }
DIV.dialog.right DIV
{ border-right: 0;
{ border-right: 0px;
margin-right: 19px;
text-align: right; }
@ -609,7 +609,7 @@ DIV#uploadResults DIV
DIV#uploadDialog H3,
DIV#uploadResults H3
{ margin-top: 0;
{ margin-top: 0px;
font-size: 1.2em; }
/* INPUT styles */
@ -650,6 +650,9 @@ INPUT.textField
vertical-align: middle;
width: 250px; }
INPUT.textField._invalid
{ background-color: #f00 !important; }
INPUT#searchValue
{ margin-right: 1em;
width: 20em;
@ -722,7 +725,7 @@ TABLE.framenocaption
{ margin-top: 14px; }
TABLE.frame TD
{ padding: 0; }
{ padding: 0px; }
SPAN.caption
{ display: inline-block;
@ -890,8 +893,8 @@ P#mailerSelectionContainer
{ line-height: 25px;
margin-top: 2px; }
DIV.bottomToolbar {
vertical-align: middle;
DIV.bottomToolbar
{ vertical-align: middle;
background-image: url("thead_bg.png");
border: 1px solid #9B9B9B;
background-color: #E6E7E6;
@ -901,9 +904,10 @@ DIV.bottomToolbar {
margin-left: 2px;
margin-right: 2px; }
DIV.bottomToolbar A.bottomButton SPAN, DIV.bottomToolbar A.bottomButton {
margin: 0;
padding: 0;
DIV.bottomToolbar A.bottomButton SPAN,
DIV.bottomToolbar A.bottomButton
{ margin: 0px;
padding: 0px;
width: 23px;
text-align: center;
text-decoration: none;
@ -912,8 +916,7 @@ DIV.bottomToolbar A.bottomButton SPAN, DIV.bottomToolbar A.bottomButton {
border-bottom: 1px solid #9B9B9B;
border-right: 1px solid #9B9B9B;
float: left;
height: 20px;
}
height: 20px; }
DIV.bottomToolbar A.bottomButton SPAN
{ background: transparent url('thead_bg.png') repeat-x top right; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B