sogo/SoObjects/SOGo/SOGoSieveManager.m

1199 lines
37 KiB
Objective-C

/* SOGoSieveManager.m - this file is part of SOGo
*
* Copyright (C) 2010-2019 Inverse inc.
*
* Author: Inverse <info@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/NSCalendarDate.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSURL.h>
#import <Foundation/NSValue.h>
#import <SOGo/NSArray+Utilities.h>
#import <SOGo/NSDictionary+Utilities.h>
#import <SOGo/NSString+Utilities.h>
#import <SOGo/SOGoDomainDefaults.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoTextTemplateFile.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+Ext.h>
#import <NGImap4/NGImap4Connection.h>
#import <NGImap4/NGImap4Client.h>
#import <NGImap4/NGSieveClient.h>
#import <NGObjWeb/NSException+HTTP.h>
#import "../Mailer/SOGoMailAccount.h"
#import "SOGoSieveManager.h"
typedef enum {
UIxFilterFieldTypeAddress,
UIxFilterFieldTypeHeader,
UIxFilterFieldTypeBody,
UIxFilterFieldTypeSize,
} UIxFilterFieldType;
static NSArray *sieveOperators = nil;
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";
@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: @"text:\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 SOGoSieveManager
+ (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: UIxFilterFieldTypeBody]
forKey: @"body"];
[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",
nil];
[sieveFlags retain];
}
if (!typeRequirements)
{
typeRequirements
= [NSDictionary dictionaryWithObjectsAndKeys:
@"body", [NSNumber numberWithInt: UIxFilterFieldTypeBody],
nil];
[typeRequirements retain];
}
if (!operatorRequirements)
{
operatorRequirements
= [NSDictionary dictionaryWithObjectsAndKeys:
@"regex", @"regex",
nil];
[operatorRequirements retain];
}
if (!methodRequirements)
{
methodRequirements
= [NSMutableDictionary dictionaryWithObjectsAndKeys:
@"imapflags", @"addflag",
@"imapflags", @"removeflag",
@"imapflags", @"flag",
@"vacation", @"vacation",
@"notify", @"notify",
@"fileinto", @"fileinto",
@"reject", @"reject",
@"regex", @"regex",
nil];
[methodRequirements retain];
}
}
+ (id) sieveManagerForUser: (SOGoUser *) newUser
{
SOGoSieveManager *newManager;
newManager = [[self alloc] initForUser: newUser];
[newManager autorelease];
return newManager;
}
- (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;
}
- (NSString *) _extractRequirementsFromContent: (NSString *) theContent
intoArray: (NSMutableArray *) theRequirements
{
NSString *line, *v;
NSArray *lines;
id o;
int i, count;
lines = [theContent componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]];
count = [lines count];
for (i = 0; i < count; i++)
{
line = [[lines objectAtIndex: i] stringByTrimmingSpaces];
if ([line hasPrefix: @"require "])
{
line = [line substringFromIndex: 8];
// Handle lines like: require "imapflags";
if ([line characterAtIndex: 0] == '"')
{
v = [line substringToIndex: [line length]-2];
[theRequirements addObject: v];
}
// Else handle lines like: require ["imapflags","vacation"];
else if ([line characterAtIndex: 0] == '[')
{
o = [[line substringToIndex: [line length]-1] objectFromJSONString];
[theRequirements addObjectsFromArray: o];
}
}
else
break;
}
return [[lines subarrayWithRange: NSMakeRange(i, count-i)] componentsJoinedByString: @"\n"];
}
- (BOOL) _extractRuleField: (NSString **) field
fromRule: (NSDictionary *) rule
andType: (UIxFilterFieldType *) type
{
NSNumber *fieldType;
NSString *jsonField, *customHeader, *requirement;
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: @"body"] ||
[jsonField isEqualToString: @"size"])
*field = nil;
else
*field = [sieveFields objectForKey: jsonField];
requirement = [typeRequirements objectForKey: fieldType];
if (requirement)
[requirements addObjectUniquely: requirement];
}
else
scriptError
= [NSString stringWithFormat: @"Rule based on unknown field '%@'",
jsonField];
}
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
// Header and Body types
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 == UIxFilterFieldTypeBody)
[sieveRule appendString: @"body :text "];
else if (type == UIxFilterFieldTypeSize)
[sieveRule appendString: @"size "];
[sieveRule appendFormat: @":%@ ", operator];
if (type == UIxFilterFieldTypeSize)
[sieveRule appendFormat: @"%@K", value];
else if (field)
[sieveRule appendFormat: @"%@ %@", field, value];
else
[sieveRule appendFormat: @"%@", 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
delimiter: (NSString *) delimiter
{
NSString *sieveAction, *method, *requirement, *argument, *flag, *mailbox;
NSDictionary *mailLabels;
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)
{
mailLabels = [[user userDefaults] mailLabelsColors];
if ([mailLabels objectForKey: argument])
flag = argument;
}
if (flag)
sieveAction = [NSString stringWithFormat: @"%@ %@",
method, [flag asSieveQuotedString]];
else
scriptError
= [NSString stringWithFormat:
@"Action with invalid flag argument '%@'",
argument];
}
else if ([method isEqualToString: @"fileinto"])
{
mailbox
= [[argument componentsSeparatedByString: @"/"]
componentsJoinedByString: delimiter];
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: @"%@ %@",
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
delimiter: (NSString *) delimiter
{
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]
delimiter: delimiter];
if (!scriptError)
[sieveActions addObject: sieveAction];
}
return sieveActions;
}
- (NSString *) _convertScriptToSieve: (NSDictionary *) newScript
delimiter: (NSString *) delimiter
{
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"]
delimiter: delimiter];
if ([sieveActions count])
[sieveText appendFormat: @" %@;\r\n",
[sieveActions componentsJoinedByString: @";\r\n "]];
if (match)
[sieveText appendFormat: @"}\r\n"];
return sieveText;
}
- (NSString *) sieveScriptWithRequirements: (NSMutableArray *) newRequirements
delimiter: (NSString *) delimiter
{
NSMutableString *sieveScript;
NSString *sieveText;
NSArray *scripts;
int count, max;
NSDictionary *currentScript;
sieveScript = [NSMutableString string];
ASSIGN(requirements, newRequirements);
[scriptError release];
scriptError = nil;
scripts = [[user userDefaults] sieveFilters];
max = [scripts count];
if (max)
{
for (count = 0; !scriptError && count < max; count++)
{
currentScript = [scripts objectAtIndex: count];
if ([[currentScript objectForKey: @"active"] boolValue])
{
sieveText = [self _convertScriptToSieve: currentScript
delimiter: delimiter];
[sieveScript appendString: sieveText];
}
}
}
[scriptError retain];
DESTROY(requirements);
if (scriptError)
sieveScript = nil;
return sieveScript;
}
- (NSString *) lastScriptError
{
return scriptError;
}
//
//
//
- (NGSieveClient *) clientForAccount: (SOGoMailAccount *) theAccount
{
return [self clientForAccount: theAccount withUsername: nil andPassword: nil];
}
//
//
//
- (NGSieveClient *) clientForAccount: (SOGoMailAccount *) theAccount
withUsername: (NSString *) theUsername
andPassword: (NSString *) thePassword
{
NSDictionary *result;
NSString *login, *authname, *password;
SOGoDomainDefaults *dd;
NGSieveClient *client;
NSString *sieveServer, *sieveScheme, *sieveQuery, *imapServer;
NSURL *url, *cUrl;
int sievePort;
BOOL connected;
dd = [user domainDefaults];
connected = YES;
// Extract credentials from mail account
login = [[theAccount imap4URL] user];
if (!theUsername && !thePassword)
{
authname = [[theAccount imap4URL] user];
password = [theAccount imap4PasswordRenewed: NO];
}
else
{
authname = theUsername;
password = thePassword;
}
// We connect to our Sieve server and check capabilities, in order
// to generate the right script, based on capabilities
//
// sieveServer might have the following format:
//
// sieve://localhost
// sieve://localhost:4190
// sieve://localhost:4190/?tls=YES
//
// Values such as "localhost" or "localhost:4190" are NOT supported.
//
// We first try to get the user's preferred Sieve server
sieveServer = [[[user mailAccounts] objectAtIndex: 0] objectForKey: @"sieveServerName"];
imapServer = [[[user mailAccounts] objectAtIndex: 0] objectForKey: @"serverName"];
cUrl = [NSURL URLWithString: (sieveServer ? sieveServer : @"")];
if ([dd sieveServer] && [[dd sieveServer] length] > 0)
url = [NSURL URLWithString: [dd sieveServer]];
else
url = [NSURL URLWithString: @"localhost"];
if ([cUrl host])
sieveServer = [cUrl host];
if (!sieveServer && [url host])
sieveServer = [url host];
if (!sieveServer && [dd sieveServer])
sieveServer = [dd sieveServer];
if (!sieveServer && imapServer)
sieveServer = [[NSURL URLWithString: imapServer] host];
if (!sieveServer)
sieveServer = @"localhost";
sieveScheme = [cUrl scheme] ? [cUrl scheme] : [url scheme];
if (!sieveScheme)
sieveScheme = @"sieve";
if ([cUrl port])
sievePort = [[cUrl port] intValue];
else
if ([url port])
sievePort = [[url port] intValue];
else
sievePort = 4190;
sieveQuery = [cUrl query] ? [cUrl query] : [url query];
if (sieveQuery)
sieveQuery = [NSString stringWithFormat: @"/?%@", sieveQuery];
else
sieveQuery = @"";
url = [NSURL URLWithString: [NSString stringWithFormat: @"%@://%@:%d%@",
sieveScheme, sieveServer, sievePort, sieveQuery]];
client = [[NGSieveClient alloc] initWithURL: url];
if (!client) {
[self errorWithFormat: @"Sieve connection failed on %@", [url description]];
return nil;
}
if (!password) {
[client closeConnection];
return nil;
}
NS_DURING
{
result = [client login: login authname: authname password: password];
}
NS_HANDLER
{
connected = NO;
}
NS_ENDHANDLER
if (!connected)
{
[self errorWithFormat: @"Sieve connection failed on %@", [url description]];
return nil;
}
if (![[result valueForKey:@"result"] boolValue] && !theUsername && !thePassword) {
[self logWithFormat: @"failure. Attempting with a renewed password (no authname supported)"];
password = [theAccount imap4PasswordRenewed: YES];
result = [client login: login password: password];
}
if (![[result valueForKey:@"result"] boolValue]) {
[self logWithFormat: @"Could not login '%@' on Sieve server: %@: %@",
login, client, result];
[client closeConnection];
return nil;
}
return [client autorelease];
}
- (BOOL) hasActiveExternalSieveScripts: (NGSieveClient *) client
{
NSDictionary *scripts;
NSEnumerator *keys;
NSString *key;
scripts = [client listScripts];
keys = [scripts keyEnumerator];
while ((key = [keys nextObject]))
{
if ([key caseInsensitiveCompare: @"sogo"] != NSOrderedSame &&
[[scripts objectForKey: key] intValue] > 0)
return YES;
}
return NO;
}
//
//
//
- (NSException *) updateFiltersForAccount: (SOGoMailAccount *) theAccount
{
return [self updateFiltersForAccount: theAccount
withUsername: nil
andPassword: nil
forceActivation: NO];
}
//
//
//
- (NSException *) updateFiltersForAccount: (SOGoMailAccount *) theAccount
withUsername: (NSString *) theUsername
andPassword: (NSString *) thePassword
forceActivation: (BOOL) forceActivation
{
NSString *filterScript, *v, *delimiter, *content, *message;
NSMutableArray *req;
NSMutableString *script, *header;
NSDictionary *result, *values;
NSException *error;
SOGoUserDefaults *ud;
SOGoDomainDefaults *dd;
NGSieveClient *client;
NGImap4Client *imapClient;
BOOL b, activate, dateCapability;
unsigned int now;
error = nil;
dd = [user domainDefaults];
if (!([dd sieveScriptsEnabled] || [dd vacationEnabled] || [dd forwardEnabled]))
return error;
req = [NSMutableArray arrayWithCapacity: 15];
ud = [user userDefaults];
client = [self clientForAccount: theAccount withUsername: theUsername andPassword: thePassword];
if (!client)
{
error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
reason: @"Error while connecting to Sieve server."];
return error;
}
// Activate script Sieve when forced or when no external script is enabled
activate = forceActivation || ![self hasActiveExternalSieveScripts: client];
// We adjust the "methodRequirements" based on the server's
// capabilities. Cyrus exposes "imapflags" while Dovecot (and
// potentially others) expose "imap4flags" as specified in RFC5332
if ([client hasCapability: @"imap4flags"])
{
[methodRequirements setObject: @"imap4flags" forKey: @"addflag"];
[methodRequirements setObject: @"imap4flags" forKey: @"removeflag"];
[methodRequirements setObject: @"imap4flags" forKey: @"flag"];
}
dateCapability = [client hasCapability: @"date"] && [client hasCapability: @"relational"];
//
// Now let's generate the script
//
script = [NSMutableString string];
// We grab the IMAP4 delimiter using the supplied username/password
if (thePassword)
{
imapClient = [NGImap4Client clientWithURL: [theAccount imap4URL]];
[imapClient login: theUsername password: thePassword];
}
else
imapClient = [[theAccount imap4Connection] client];
delimiter = [imapClient delimiter];
if (!delimiter)
[imapClient list: @"INBOX" pattern: @""];
delimiter = [imapClient delimiter];
if (!delimiter)
delimiter = [dd stringForKey: @"NGImap4ConnectionStringSeparator"];
// We first handle filters
filterScript = [self sieveScriptWithRequirements: req
delimiter: delimiter];
if (filterScript)
{
if ([filterScript length])
{
b = YES;
[script appendString: filterScript];
}
}
else
{
message = [NSString stringWithFormat: @"Sieve generation failure: %@", [self lastScriptError]];
[self errorWithFormat: message];
[client closeConnection];
error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
reason: message];
return error;
}
//
// We handle vacation messages.
// See http://ietfreport.isoc.org/idref/draft-ietf-sieve-vacation/
//
values = [ud vacationOptions];
now = [[NSCalendarDate calendarDate] timeIntervalSince1970];
if (values && [[values objectForKey: @"enabled"] boolValue] &&
(![[values objectForKey: @"startDateEnabled"] boolValue] ||
dateCapability || [[values objectForKey: @"startDate"] intValue] < now) &&
(![[values objectForKey: @"endDateEnabled"] boolValue] ||
dateCapability || [[values objectForKey: @"endDate"] intValue] > now))
{
NSCalendarDate *startDate, *endDate;
NSMutableArray *allConditions;
NSMutableString *vacation_script;
NSArray *addresses;
NSString *text, *templateFilePath, *customSubject;
SOGoTextTemplateFile *templateFile;
BOOL ignore, alwaysSend, useCustomSubject, discardMails;
int days, i;
allConditions = [NSMutableArray array];
days = [[values objectForKey: @"daysBetweenResponse"] intValue];
addresses = [values objectForKey: @"autoReplyEmailAddresses"];
alwaysSend = [[values objectForKey: @"alwaysSend"] boolValue];
discardMails = [[values objectForKey: @"discardMails"] boolValue];
ignore = [[values objectForKey: @"ignoreLists"] boolValue];
useCustomSubject = [[values objectForKey: @"customSubjectEnabled"] boolValue];
customSubject = [values objectForKey: @"customSubject"];
text = [values objectForKey: @"autoReplyText"];
b = YES;
if (!text)
text = @"";
if (!useCustomSubject)
{
// If user has not specified a custom subject, fallback to the domain's defaults
customSubject = [dd vacationDefaultSubject];
useCustomSubject = [customSubject length] > 0;
}
/* Add autoresponder header if configured */
templateFilePath = [dd vacationHeaderTemplateFile];
if (templateFilePath)
{
templateFile = [SOGoTextTemplateFile textTemplateFromFile: templateFilePath];
if (templateFile)
text = [NSString stringWithFormat: @"%@%@", [templateFile textForUser: user], text];
}
/* Add autoresponder footer if configured */
templateFilePath = [dd vacationFooterTemplateFile];
if (templateFilePath)
{
templateFile = [SOGoTextTemplateFile textTemplateFromFile: templateFilePath];
if (templateFile)
text = [NSString stringWithFormat: @"%@%@", text, [templateFile textForUser: user]];
}
if (days == 0)
days = 7;
vacation_script = [NSMutableString string];
[req addObjectUniquely: @"vacation"];
// Skip mailing lists
if (ignore)
{
[allConditions addObject: @"not exists [\"list-help\", \"list-unsubscribe\", \"list-subscribe\", \"list-owner\", \"list-post\", \"list-archive\", \"list-id\", \"Mailing-List\"]"];
[allConditions addObject: @"not header :comparator \"i;ascii-casemap\" :is \"Precedence\" [\"list\", \"bulk\", \"junk\"]"];
[allConditions addObject: @"not header :comparator \"i;ascii-casemap\" :matches \"To\" \"Multiple recipients of*\""];
}
// Start date of auto-reply
if ([dd vacationPeriodEnabled] &&
[[values objectForKey: @"startDateEnabled"] boolValue] &&
dateCapability)
{
[req addObjectUniquely: @"date"];
[req addObjectUniquely: @"relational"];
startDate = [NSCalendarDate dateWithTimeIntervalSince1970:
[[values objectForKey: @"startDate"] intValue]];
[allConditions addObject: [NSString stringWithFormat: @"currentdate :value \"ge\" \"date\" \"%@\"",
[startDate descriptionWithCalendarFormat: @"%Y-%m-%d"]]];
}
// End date of auto-reply
if ([dd vacationPeriodEnabled] &&
[[values objectForKey: @"endDateEnabled"] boolValue] &&
dateCapability)
{
[req addObjectUniquely: @"date"];
[req addObjectUniquely: @"relational"];
endDate = [NSCalendarDate dateWithTimeIntervalSince1970:
[[values objectForKey: @"endDate"] intValue]];
[allConditions addObject: [NSString stringWithFormat: @"currentdate :value \"le\" \"date\" \"%@\"",
[endDate descriptionWithCalendarFormat: @"%Y-%m-%d"]]];
}
// Apply conditions
if ([allConditions count])
[vacation_script appendFormat: @"if allof ( %@ ) { ",
[allConditions componentsJoinedByString: @", "]];
// Custom subject
if (useCustomSubject)
{
if (([customSubject rangeOfString: @"${subject}"].location != NSNotFound) &&
[client hasCapability: @"variables"])
{
[req addObjectUniquely: @"variables"];
[vacation_script appendString: @"if header :matches \"Subject\" \"*\" { set \"subject\" \"${1}\"; } "];
}
}
[vacation_script appendFormat: @"vacation :days %d", days];
if (useCustomSubject)
[vacation_script appendFormat: @" :subject %@", [customSubject doubleQuotedString]];
[vacation_script appendString: @" :addresses ["];
for (i = 0; i < [addresses count]; i++)
{
[vacation_script appendFormat: @"\"%@\"", [addresses objectAtIndex: i]];
if (i == [addresses count]-1)
[vacation_script appendString: @"] "];
else
[vacation_script appendString: @", "];
}
[vacation_script appendFormat: @"text:\r\n%@\r\n.\r\n;\r\n", text];
// Should we discard incoming mails during vacation?
if (discardMails)
[vacation_script appendString: @"discard;\r\n"];
// Closing bracket of conditions
if ([allConditions count])
[vacation_script appendString: @"}\r\n"];
//
// See https://sogo.nu/bugs/view.php?id=2332 for details
//
if (alwaysSend)
[script insertString: vacation_script atIndex: 0];
else
[script appendString: vacation_script];
}
// We handle mail forward
values = [ud forwardOptions];
if (values && [[values objectForKey: @"enabled"] boolValue])
{
id addresses;
int i;
b = YES;
addresses = [values objectForKey: @"forwardAddress"];
if ([addresses isKindOfClass: [NSString class]])
addresses = [addresses componentsSeparatedByString: @","];
for (i = 0; i < [addresses count]; i++)
{
v = [addresses objectAtIndex: i];
if (v && [v length] > 0)
[script appendFormat: @"redirect \"%@\";\r\n", v];
}
if ([[values objectForKey: @"keepCopy"] boolValue])
[script appendString: @"keep;\r\n"];
}
// We handle header/footer Sieve scripts
if ((v = [dd sieveScriptHeaderTemplateFile]))
{
content = [NSString stringWithContentsOfFile: v
encoding: NSUTF8StringEncoding
error: NULL];
if (content)
{
v = [self _extractRequirementsFromContent: content
intoArray: req];
[script insertString: v atIndex: 0];
b = YES;
}
}
if ((v = [dd sieveScriptFooterTemplateFile]))
{
content = [NSString stringWithContentsOfFile: v
encoding: NSUTF8StringEncoding
error: NULL];
if (content)
{
v = [self _extractRequirementsFromContent: content
intoArray: req];
[script appendString: @"\n"];
[script appendString: v];
b = YES;
}
}
if ([req count])
{
header = [NSString stringWithFormat: @"require [\"%@\"];\r\n",
[[req uniqueObjects] componentsJoinedByString: @"\",\""]];
[script insertString: header atIndex: 0];
}
/* We ensure to deactive the current active script since it could prevent
its deletion from the server. */
if (activate)
result = [client setActiveScript: @""];
// We delete the existing Sieve script
result = [client deleteScript: sieveScriptName];
if (![[result valueForKey:@"result"] boolValue])
[self warnWithFormat: @"Could not delete Sieve script: %@", [[result objectForKey: @"RawResponse"] objectForKey: @"reason"]];
/* We put and activate the script only if we actually have a script
that does something... */
if (b && [script length])
{
result = [client putScript: sieveScriptName script: script];
if (![[result valueForKey:@"result"] boolValue])
{
message = [NSString stringWithFormat: @"Could not upload Sieve script: %@", [[result objectForKey: @"RawResponse"] objectForKey: @"reason"]];
[self errorWithFormat: message];
[client closeConnection];
error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
reason: message];
return error;
}
if (activate)
{
result = [client setActiveScript: sieveScriptName];
if (![[result valueForKey:@"result"] boolValue])
{
message = [NSString stringWithFormat: @"Could not enable Sieve script: %@", [[result objectForKey: @"RawResponse"] objectForKey: @"reason"]];
[self errorWithFormat: message];
[client closeConnection];
error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
reason: message];
return error;
}
}
}
[client closeConnection];
return error;
}
@end