/*
|
|
** CWIMAPStore.m
|
|
**
|
|
** Copyright (c) 2001-2007
|
|
**
|
|
** Author: Ludovic Marcotte <ludovic@Sophos.ca>
|
|
**
|
|
** This library is free software; you can redistribute it and/or
|
|
** modify it under the terms of the GNU Lesser General Public
|
|
** License as published by the Free Software Foundation; either
|
|
** version 2.1 of the License, or (at your option) any later version.
|
|
**
|
|
** This library 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
|
|
** Lesser General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU Lesser General Public
|
|
** License along with this library; if not, write to the Free Software
|
|
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*/
|
|
|
|
#import "CWIMAPStore+Protected.h"
|
|
|
|
#import <pEpIOSToolboxForExtensions/PEPLogger.h>
|
|
#import "CWConstants.h"
|
|
#import "CWFlags.h"
|
|
#import "Pantomime/CWFolderInformation.h"
|
|
#import "CWIMAPFolder.h"
|
|
#import "CWIMAPMessage.h"
|
|
#import "Pantomime/CWMD5.h"
|
|
#import "CWMIMEUtility.h"
|
|
#import "Pantomime/CWURLName.h"
|
|
#import "NSData+Extensions.h"
|
|
#import "Pantomime/NSScanner+Extensions.h"
|
|
#import "Pantomime/NSString+Extensions.h"
|
|
|
|
|
|
#import <Foundation/NSAutoreleasePool.h>
|
|
#import <Foundation/NSBundle.h>
|
|
#import <Foundation/NSCharacterSet.h>
|
|
#import <Foundation/NSException.h>
|
|
#import <Foundation/NSNotification.h>
|
|
#import <Foundation/NSPathUtilities.h>
|
|
#import <Foundation/NSScanner.h>
|
|
#import <Foundation/NSValue.h>
|
|
|
|
#import "CWIMAPCacheManager.h"
|
|
#import "CWThreadSafeArray.h"
|
|
#import "CWThreadSafeData.h"
|
|
|
|
#import "NSDate+StringRepresentation.h"
|
|
|
|
#import <ctype.h>
|
|
#import <stdio.h>
|
|
|
|
#import "CWOAuthUtils.h"
|
|
#import "CWService+Protected.h"
|
|
#import "CWIMAPFolder+CWProtected.h"
|
|
|
|
//
|
|
// This C function is used to verify if a line (specified in
|
|
// "buf", with length "c") has a literal. If it does, the
|
|
// value of the literal is returned.
|
|
//
|
|
// "0" means no literal.
|
|
//
|
|
static inline int has_literal(char *buf, NSUInteger c)
|
|
{
|
|
char *s;
|
|
|
|
if (c == 0 || *buf != '*') return 0;
|
|
|
|
s = buf+c-1;
|
|
|
|
if (*s == '}')
|
|
{
|
|
int value, d;
|
|
|
|
value = 0;
|
|
d = 1;
|
|
s--;
|
|
|
|
while (isdigit((int)(unsigned char)*s))
|
|
{
|
|
value += ((*s-48) * d);
|
|
d *= 10;
|
|
s--;
|
|
}
|
|
|
|
//LogInfo(@"LITERAL = %d", value);
|
|
|
|
return value;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
@interface CWIMAPStore ()
|
|
|
|
/**
|
|
Regular expression for extracting the UID from a FETCH response.
|
|
*/
|
|
@property (strong, nonatomic, nonnull) NSRegularExpression *uidRegex;
|
|
|
|
@end
|
|
|
|
//
|
|
// Private methods
|
|
//
|
|
@interface CWIMAPStore (Private)
|
|
|
|
- (void) _parseAUTHENTICATE_CRAM_MD5;
|
|
- (void) _parseAUTHENTICATE_LOGIN;
|
|
- (void) _parseBAD;
|
|
- (void) _parseBYE;
|
|
- (void) _parseCAPABILITY;
|
|
- (void) _parseEXISTS;
|
|
- (void) _parseEXPUNGE;
|
|
- (void) _parseFETCH_UIDS;
|
|
- (void) _parseFETCH: (NSInteger) theMSN;
|
|
- (void) _parseLIST;
|
|
- (void) _parseLSUB;
|
|
- (void) _parseNO;
|
|
- (void) _parseNOOP;
|
|
- (void) _parseOK;
|
|
- (void) _parseRECENT;
|
|
- (void) _parseSEARCH;
|
|
- (void) _parseSEARCH_CACHE;
|
|
- (void) _parseSELECT;
|
|
- (void) _parseSTATUS;
|
|
- (void) _parseSTARTTLS;
|
|
- (void) _parseUIDVALIDITY: (const char *) theString;
|
|
- (void) _restoreQueue;
|
|
|
|
@end
|
|
|
|
//
|
|
//
|
|
//
|
|
@implementation CWIMAPStore
|
|
|
|
//
|
|
//
|
|
//
|
|
+ (BOOL)accessInstanceVariablesDirectly
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (instancetype) initWithName: (NSString *) theName
|
|
port: (unsigned int) thePort
|
|
transport: (ConnectionTransport)transport
|
|
clientCertificate: (SecIdentityRef _Nullable)clientCertificate
|
|
{
|
|
if (thePort == 0) thePort = 143;
|
|
|
|
self = [super initWithName:theName
|
|
port:thePort
|
|
transport:transport
|
|
clientCertificate:clientCertificate];
|
|
|
|
LogInfo(@"CWIMAPStore.init %@", self);
|
|
|
|
_folderSeparator = 0;
|
|
_selectedFolder = nil;
|
|
_tag = 1;
|
|
|
|
_folders = [[NSMutableDictionary alloc] init];
|
|
_openFolders = [[NSMutableDictionary alloc] init];
|
|
_subscribedFolders = [[NSMutableArray alloc] init];
|
|
_folderStatus = [[NSMutableDictionary alloc] init];
|
|
|
|
_lastCommand = IMAP_AUTHORIZATION;
|
|
_currentQueueObject = nil;
|
|
|
|
NSError *error;
|
|
_uidRegex = [NSRegularExpression
|
|
regularExpressionWithPattern:@".*UID (\\d+).*"
|
|
options: 0 error: &error];
|
|
assert(error == nil);
|
|
|
|
return self;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) dealloc
|
|
{
|
|
LogInfo(@"dealloc %@", self);
|
|
RELEASE(_folders);
|
|
RELEASE(_folderStatus);
|
|
RELEASE(_openFolders);
|
|
RELEASE(_subscribedFolders);
|
|
//[super dealloc];
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void)exitIDLE
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
if (self.lastCommand == IMAP_IDLE) {
|
|
_lastCommand = IMAP_IDLE_DONE;
|
|
[self writeData: [[NSData alloc] initWithBytes: "DONE\r\n" length: 6]];
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) sendCommand: (IMAPCommand) theCommand info: (NSDictionary * _Nullable) theInfo
|
|
string:(NSString * _Nonnull)theString
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[self sendCommandInternal:theCommand info:theInfo string:theString];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSArray *) supportedMechanisms
|
|
{
|
|
__block NSArray *returnee = nil;
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
NSMutableArray *aMutableArray;
|
|
NSString *aString;
|
|
NSUInteger i, count;;
|
|
|
|
aMutableArray = [NSMutableArray array];
|
|
count = [strongSelf->_capabilities count];
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
aString = [strongSelf->_capabilities objectAtIndex: i];
|
|
|
|
if ([aString hasCaseInsensitivePrefix: @"AUTH="])
|
|
{
|
|
[aMutableArray addObject: [aString substringFromIndex: 5]];
|
|
}
|
|
}
|
|
|
|
returnee = aMutableArray;
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
////! VERIFY FOR NoSelect
|
|
- (CWIMAPFolder *) folderForName: (NSString *) theName
|
|
mode: (PantomimeFolderMode) theMode
|
|
updateExistsCount: (BOOL)updateExistsCount
|
|
{
|
|
__block CWIMAPFolder *returnee = nil;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
returnee = [self folderForNameInternal:theName
|
|
mode:theMode
|
|
updateExistsCount:updateExistsCount];
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
#pragma mark - Overriden
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) cancelRequest
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[super cancelRequest];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) close
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
// ignore all subsequent messages from the servers
|
|
strongSelf->_delegate = nil;
|
|
|
|
[strongSelf->_openFolders removeAllObjects];
|
|
|
|
if (strongSelf->_connected) {
|
|
[strongSelf sendCommand: IMAP_LOGOUT info: nil arguments: @"LOGOUT"];
|
|
}
|
|
[super close];
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
When this method is called, we are receiving bytes
|
|
from the _lastCommand.
|
|
|
|
Rationale:
|
|
|
|
This command accumulates the responses (split into lines)
|
|
from the server in _responsesFromServer.
|
|
|
|
It will NOT add untagged reponses but rather process them right-away.
|
|
|
|
If it's receiving a FETCH response, it will NOT verify for
|
|
tag line ('0123 OK', '0123 BAD', '0123 NO') for the duration
|
|
of reading the literal length. For example, if we got {400},
|
|
we will not consider a '0123 OK' response if we read less
|
|
than 400 bytes. This prevent us from reading a '0123 OK' that
|
|
could occur in a message.
|
|
*/
|
|
- (void) updateRead
|
|
{
|
|
NSData *aData;
|
|
|
|
NSUInteger i, count;
|
|
char *buf;
|
|
|
|
[super updateRead];
|
|
|
|
//LogInfo(@"_rbul len == %d |%@|", [_rbuf length], [_rbuf asciiString]);
|
|
|
|
if (![_rbuf length]) return;
|
|
|
|
while ((aData = [_rbuf dropFirstLine]))
|
|
{
|
|
//LogInfo(@"aLine = |%@|", [aData asciiString]);
|
|
buf = (char *)[aData bytes];
|
|
count = [aData length];
|
|
|
|
// If we are reading a literal, do so.
|
|
if (self.currentQueueObject && self.currentQueueObject.literal)
|
|
{
|
|
self.currentQueueObject.literal -= (int) (count+2);
|
|
//LogInfo(@"literal = %d, count = %d", self.currentQueueObject.literal, count);
|
|
|
|
if (self.currentQueueObject.literal < 0)
|
|
{
|
|
int x;
|
|
|
|
x = -2-self.currentQueueObject.literal;
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] appendData: [aData subdataToIndex: x]];
|
|
[_responsesFromServer addObject: [aData subdataFromIndex: x]];
|
|
//LogInfo(@"orig = |%@|, chooped = |%@| |%@|", [aData asciiString], [[aData subdataToIndex: x] asciiString], [[aData subdataFromIndex: x] asciiString]);
|
|
}
|
|
else
|
|
{
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] appendData: aData];
|
|
}
|
|
|
|
// We are done reading a literal. Let's read again
|
|
// to see if we got a full response.
|
|
if (self.currentQueueObject.literal <= 0)
|
|
{
|
|
//LogInfo(@"DONE ACCUMULATING LITTERAL!\nread = |%@|", [[self.currentQueueObject.info objectForKey: @"NSData"] asciiString]);
|
|
//
|
|
// Let's see, if we can, what does the next line contain. If we got
|
|
// something, we add this to the remaining _responsesFromServer
|
|
// and we are ready to parse that response (_responsesFromServer + bytes of literal).
|
|
//
|
|
// If it's nil, that's because we have nothing to read. In that case, just loop
|
|
// and call -updateRead in order to read the rest of the response.
|
|
//
|
|
// We must also be careful about what we read. Microsoft Exchange sometimes send us
|
|
// stuff like this:
|
|
//
|
|
// * 5 FETCH (BODY[TEXT] {1175}
|
|
// <!DOCTYPE HTML ...
|
|
// ...
|
|
// </HTML> UID 5)
|
|
// 0010 OK FETCH completed.
|
|
//
|
|
// The "</HTML> UID 5)" line will result in a _negative_ literal. Which we
|
|
// handle well here and just a couple of lines above this one.
|
|
//
|
|
if (self.currentQueueObject.literal < 0)
|
|
{
|
|
self.currentQueueObject.literal = 0;
|
|
}
|
|
else
|
|
{
|
|
// We MUST wait until we are done reading our full
|
|
// FETCH response. _rbuf could end immediately at the
|
|
// end of our literal response and we need to call
|
|
// [super updateRead] to get more bytes from the socket
|
|
// in order to read the rest (")" or " UID 123)" for example).
|
|
while (!(aData = [_rbuf dropFirstLine]))
|
|
{
|
|
//SLog(@"NOTHING TO READ! WAITING...");
|
|
[super updateRead];
|
|
}
|
|
[_responsesFromServer addObject: aData];
|
|
}
|
|
|
|
//
|
|
// Let's rollback in what are processing/read in order to
|
|
// reparse our initial response. It's if it's FETCH response,
|
|
// the literal will now be 0 so the parsing of this response
|
|
// will occur.
|
|
//
|
|
aData = [_responsesFromServer objectAtIndex: 0];
|
|
buf = (char *)[aData bytes];
|
|
count = [aData length];
|
|
}
|
|
else
|
|
{
|
|
//LogInfo(@"Accumulating... %d remaining...", self.currentQueueObject.literal);
|
|
//
|
|
// We are still accumulating bytes of the literal. Once we have appended
|
|
// our CRLF, we just continue the loop since there's no need to try to
|
|
// parse anything, as we don't have the complete response yet.
|
|
//
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] appendData: _crlf];
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//LogInfo(@"aLine = |%@|", [aData asciiString]);
|
|
[_responsesFromServer addObject: aData];
|
|
|
|
if (self.currentQueueObject && (self.currentQueueObject.literal = has_literal(buf, count)))
|
|
{
|
|
//LogInfo(@"literal = %d", self.currentQueueObject.literal);
|
|
[self.currentQueueObject.info setObject: [NSMutableData dataWithCapacity: self.currentQueueObject.literal]
|
|
forKey: @"NSData"];
|
|
}
|
|
}
|
|
|
|
// Now search for the position of the first space in our response.
|
|
i = 0;
|
|
while (i < count && *buf != ' ')
|
|
{
|
|
buf++; i++;
|
|
}
|
|
|
|
//LogInfo(@"i = %d count = %d", i, count);
|
|
|
|
//
|
|
// We got an untagged response or a command continuation request.
|
|
//
|
|
if (i == 1)
|
|
{
|
|
NSInteger d, j, msn, len;
|
|
BOOL b;
|
|
|
|
//
|
|
// We verify if we received a command continuation request.
|
|
// This response is used in the AUTHENTICATE command or
|
|
// in any argument to the command is a literal. In the current
|
|
// code, the only command which has a literal argument is
|
|
// the APPEND command. We must NOT use "break;" at the very
|
|
// end of this block since we could read a line in a mail
|
|
// that begins with a '+'.
|
|
//
|
|
if (*(buf-i) == '+')
|
|
{
|
|
if (self.currentQueueObject && _lastCommand == IMAP_APPEND)
|
|
{
|
|
[self bulkWriteData:@[[self.currentQueueObject.info objectForKey: @"NSDataToAppend"],
|
|
_crlf]];
|
|
break;
|
|
}
|
|
else if (_lastCommand == IMAP_AUTHENTICATE_CRAM_MD5)
|
|
{
|
|
[self _parseAUTHENTICATE_CRAM_MD5];
|
|
break;
|
|
}
|
|
else if (_lastCommand == IMAP_AUTHENTICATE_LOGIN)
|
|
{
|
|
[self _parseAUTHENTICATE_LOGIN];
|
|
break;
|
|
}
|
|
|
|
// IMAP_AUTHENTICATE_XOAUTH2 answers with OK response in case of success.
|
|
// In case case of failure a JSON containing the status is returned, no BAD or NO
|
|
// response.
|
|
//
|
|
// Example success response from gmail:
|
|
// "0002 OK Thats all she wrote! o3mb34104947ljc"
|
|
//
|
|
// Example failure response from gmail:
|
|
// + eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczovL21haWwuZ29vZ2xlLmNvbS8ifQ==
|
|
// decoded:
|
|
// {"status":"400","schemes":"Bearer","scope":"https://mail.google.com/"}
|
|
else if (_lastCommand == IMAP_AUTHENTICATE_XOAUTH2)
|
|
{
|
|
// We ignore the status contained in the response.
|
|
// This if clause is reached only in failure case.
|
|
AUTHENTICATION_FAILED(_delegate, _mechanism);
|
|
break;
|
|
}
|
|
|
|
else if (self.currentQueueObject && _lastCommand == IMAP_LOGIN)
|
|
{
|
|
//LogInfo(@"writing password |%s|", [[self.currentQueueObject.info objectForKey: @"Password"] cString]);
|
|
[self bulkWriteData:@[[self.currentQueueObject.info objectForKey: @"Password"],
|
|
_crlf]];
|
|
break;
|
|
} else if (_lastCommand == IMAP_IDLE) {
|
|
LogInfo(@"entering IDLE");
|
|
PERFORM_SELECTOR_1(_delegate, @selector(idleEntered:), PantomimeIdleEntered);
|
|
}
|
|
}
|
|
|
|
msn = 0; b = YES; d = 1;
|
|
j = i+1; buf++;
|
|
|
|
// Let's see if we can read a MSN
|
|
while (j < count && *buf != ' ')
|
|
{
|
|
if (!isdigit((int)(unsigned char)*buf)) b = NO;
|
|
buf++; j++;
|
|
}
|
|
|
|
//LogInfo(@"j = %d, b = %d", j, b);
|
|
|
|
//
|
|
// The token following our "*" is all-digit. Let's
|
|
// decode the MSN and get the kind of response.
|
|
//
|
|
// We will also read the untagged responses we get
|
|
// when SELECT'ing a mailbox ("* 4 EXISTS" for example).
|
|
//
|
|
// We parse those results but we ignore the "MSN" since
|
|
// it bears no relation to an actual MSN.
|
|
//
|
|
if (b)
|
|
{
|
|
NSInteger k;
|
|
|
|
k = j;
|
|
|
|
// We compute the MSN
|
|
while (k > i+1)
|
|
{
|
|
buf--; k--;
|
|
//LogInfo(@"msn c = %c", *buf);
|
|
msn += ((*buf-48) * d);
|
|
d *= 10;
|
|
}
|
|
|
|
//LogInfo(@"Done computing the msn = %d k = %d", msn, k);
|
|
|
|
// We now get what kind of response we read (FETCH, etc?)
|
|
buf += (j-i);
|
|
k = j+1;
|
|
|
|
while (k < count && isalpha((int)(unsigned char)*buf))
|
|
{
|
|
//LogInfo(@"response after c = %c", *buf);
|
|
buf++; k++;
|
|
}
|
|
|
|
//LogInfo(@"Done reading response: i = %d j = %d k = %d", i, j, k);
|
|
|
|
buf = buf-k+j+1;
|
|
len = k-j-1;
|
|
}
|
|
//
|
|
// It's NOT all-digit.
|
|
//
|
|
else
|
|
{
|
|
buf = buf-j+i+1;
|
|
len = j-i-1;
|
|
}
|
|
|
|
//NSData *foo;
|
|
//foo = [NSData dataWithBytes: buf length: len];
|
|
//LogInfo(@"DONE!!! foo after * = |%@| b = %d, msn = %d", [foo asciiString], b, msn);
|
|
//LogInfo(@"len = %d", len);
|
|
|
|
//
|
|
// We got an untagged OK response. We handle only the one used in the IMAP authorization
|
|
// state and ignore the ones required during a SELECT command (like OK [UNSEEN <n>]).
|
|
//
|
|
if (len && strncasecmp("OK", buf, 2) == 0 && _lastCommand == IMAP_AUTHORIZATION)
|
|
{
|
|
[self _parseOK];
|
|
}
|
|
//
|
|
// We got a BAD response without sequence number.
|
|
// Example: "* BAD internal server error"
|
|
// Stop parsing. _parseBAD is responsable for handling it.
|
|
//
|
|
else if (len && strncasecmp("BAD", buf, 3) == 0)
|
|
{
|
|
[self _parseBAD];
|
|
}
|
|
//
|
|
// We check if we got disconnected from the IMAP server.
|
|
// If it's the case, we invoke -reconnect.
|
|
//
|
|
else if (len && strncasecmp("BYE", buf, 3) == 0)
|
|
{
|
|
[self _parseBYE];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("LIST", buf, 4) == 0)
|
|
{
|
|
[self _parseLIST];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("LSUB", buf, 4) == 0)
|
|
{
|
|
[self _parseLSUB];
|
|
}
|
|
//
|
|
// We got a FETCH response and we are done reading all
|
|
// bytes specified by our literal. We also handle
|
|
// untagged responses coming AFTER a tagged response,
|
|
// like that:
|
|
//
|
|
// 000c UID FETCH 3071053:3071053 BODY[TEXT]
|
|
// * 1 FETCH (UID 3071053 BODY[TEXT] {859}
|
|
// f00 bar zarb
|
|
// ..
|
|
// )
|
|
// 000c OK UID FETCH completed
|
|
// * 1 FETCH (FLAGS (\Seen))
|
|
//
|
|
// Responses like that must be carefully handled since
|
|
// self.currentQueueObject would nil after getting the
|
|
// tagged response.
|
|
//
|
|
else if (len && strncasecmp("FETCH", buf, 5) == 0 &&
|
|
(!self.currentQueueObject || (self.currentQueueObject && self.currentQueueObject.literal == 0)))
|
|
{
|
|
switch (_lastCommand)
|
|
{
|
|
case IMAP_UID_FETCH_UIDS:
|
|
[self _parseFETCH_UIDS];
|
|
break;
|
|
default:
|
|
[self _parseFETCH: msn];
|
|
}
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("EXISTS", buf, 6) == 0)
|
|
{
|
|
[self _parseEXISTS];
|
|
[_responsesFromServer removeLastObject];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("RECENT", buf, 6) == 0)
|
|
{
|
|
[self _parseRECENT];
|
|
[_responsesFromServer removeLastObject];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("SEARCH", buf, 6) == 0)
|
|
{
|
|
switch (_lastCommand)
|
|
{
|
|
case IMAP_UID_SEARCH:
|
|
case IMAP_UID_SEARCH_ANSWERED:
|
|
case IMAP_UID_SEARCH_FLAGGED:
|
|
case IMAP_UID_SEARCH_UNSEEN:
|
|
[self _parseSEARCH_CACHE];
|
|
break;
|
|
default:
|
|
[self _parseSEARCH];
|
|
}
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("STATUS", buf, 6) == 0)
|
|
{
|
|
[self _parseSTATUS];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("EXPUNGE", buf, 7) == 0)
|
|
{
|
|
[self _parseEXPUNGE];
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (len && strncasecmp("CAPABILITY", buf, 10) == 0)
|
|
{
|
|
[self _parseCAPABILITY];
|
|
}
|
|
}
|
|
//
|
|
// We got a tagged response
|
|
//
|
|
else
|
|
{
|
|
buf -= i; // go back to the beginning
|
|
|
|
// convert buf into \0-terminated string
|
|
char *tmpBuffer = malloc(count + 1);
|
|
memcpy(tmpBuffer, buf, count);
|
|
tmpBuffer[count] = '\0';
|
|
|
|
char *response = malloc(count + 1);
|
|
|
|
int itemsAssigned = sscanf(tmpBuffer, "%*s %s", response);
|
|
|
|
if (itemsAssigned != 1) {
|
|
[self _parseBAD];
|
|
} else {
|
|
if (strcmp(response, "OK") == 0) {
|
|
// From RFC3501:
|
|
//
|
|
// The server completion result response indicates the success or
|
|
// failure of the operation. It is tagged with the same tag as the
|
|
// client command which began the operation. Thus, if more than one
|
|
// command is in progress, the tag in a server completion response
|
|
// identifies the command to which the response applies. There are
|
|
// three possible server completion responses: OK (indicating success),
|
|
// NO (indicating failure), or BAD (indicating a protocol error such as
|
|
// unrecognized command or command syntax error).
|
|
//
|
|
[self _parseOK];
|
|
} else if (strcmp(response, "NO") == 0) {
|
|
//
|
|
// RFC3501 says:
|
|
//
|
|
// The NO response indicates an operational error message from the
|
|
// server. When tagged, it indicates unsuccessful completion of the
|
|
// associated command. The untagged form indicates a warning; the
|
|
// command can still complete successfully. The human-readable text
|
|
// describes the condition.
|
|
//
|
|
[self _parseNO];
|
|
} else {
|
|
[self _parseBAD];
|
|
}
|
|
|
|
free(tmpBuffer);
|
|
free(response);
|
|
}
|
|
}
|
|
} // while ((aData = split_lines...
|
|
|
|
//LogInfo(@"While loop broken!");
|
|
}
|
|
|
|
|
|
//
|
|
// This method NOOPs the IMAP store.
|
|
//
|
|
- (void) noop
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[self sendCommand: IMAP_NOOP info: nil arguments: @"NOOP"];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (int) reconnect
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
//LogInfo(@"CWIMAPStore: -reconnect");
|
|
|
|
[strongSelf->_connection_state.previous_queue addObjectsFromArray: [strongSelf->_queue array]];
|
|
strongSelf->_connection_state.reconnecting = YES;
|
|
|
|
// We flush our read/write buffers.
|
|
[strongSelf->_rbuf reset];
|
|
[strongSelf->_wbuf reset];
|
|
|
|
//
|
|
// We first empty our queue and set again our _lastCommand ivar to
|
|
// the IMAP_AUTHORIZATION command
|
|
//
|
|
//LogInfo(@"queue count = %d", [_queue count]);
|
|
//LogInfo(@"%@", [_queue description]);
|
|
[strongSelf->_queue removeAllObjects];
|
|
strongSelf->_lastCommand = IMAP_AUTHORIZATION;
|
|
LogInfo(@"reconnect currentQueueObject = nil");
|
|
strongSelf.currentQueueObject = nil;
|
|
strongSelf->_counter = 0;
|
|
|
|
[super close];
|
|
[super connectInBackgroundAndNotify];
|
|
});
|
|
|
|
return 0; // In case you wonder see reconnect doc: @result Pending.
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) startTLS
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[self sendCommand: IMAP_STARTTLS info: nil arguments: @"STARTTLS"];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// This method authenticates the Store to the IMAP server.
|
|
// In case of an error, it returns NO.
|
|
//
|
|
//! We MUST NOT send a login command if LOGINDISABLED is
|
|
// enforced by the server (6.2.3).
|
|
//
|
|
- (void) authenticate: (NSString*) theUsername
|
|
password: (NSString*) thePassword
|
|
mechanism: (NSString*) theMechanism
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
strongSelf->_username = theUsername;
|
|
strongSelf->_password = thePassword;
|
|
strongSelf->_mechanism = theMechanism;
|
|
|
|
// AUTH=CRAM-MD5
|
|
if (theMechanism && [theMechanism caseInsensitiveCompare: @"CRAM-MD5"] == NSOrderedSame)
|
|
{
|
|
[strongSelf sendCommand: IMAP_AUTHENTICATE_CRAM_MD5 info: nil arguments: @"AUTHENTICATE CRAM-MD5"];
|
|
return;
|
|
} // AUTH=LOGIN
|
|
else if (theMechanism && [theMechanism caseInsensitiveCompare: @"LOGIN"] == NSOrderedSame)
|
|
{
|
|
[strongSelf sendCommand: IMAP_AUTHENTICATE_LOGIN info: nil arguments: @"AUTHENTICATE LOGIN"];
|
|
return;
|
|
}
|
|
// AUTH=XOAUTH2
|
|
else if (theMechanism && [theMechanism caseInsensitiveCompare: @"XOAUTH2"] == NSOrderedSame)
|
|
{
|
|
NSString *clientResponse = [CWOAuthUtils base64EncodedClientResponseForUser:theUsername
|
|
accessToken:thePassword];
|
|
[strongSelf sendCommand: IMAP_AUTHENTICATE_XOAUTH2
|
|
info: nil
|
|
arguments: @"AUTHENTICATE XOAUTH2 %@", clientResponse];
|
|
return;
|
|
}
|
|
|
|
// Fallback to simple LOGIN (https://tools.ietf.org/html/rfc3501#section-6.2.3)
|
|
|
|
NSString *safePassword = thePassword;
|
|
|
|
// We must verify if we must quote the password
|
|
if ([thePassword rangeOfCharacterFromSet: [NSCharacterSet punctuationCharacterSet]].length ||
|
|
[thePassword rangeOfCharacterFromSet: [NSCharacterSet whitespaceCharacterSet]].length)
|
|
{
|
|
safePassword = [NSString stringWithFormat: @"\"%@\"", thePassword];
|
|
}
|
|
else if (![thePassword is7bitSafe])
|
|
{
|
|
NSData *aData;
|
|
|
|
//
|
|
// We support non-ASCII password by using the 8-bit ISO Latin 1 encoding.
|
|
//! Is there any standard on which encoding to use?
|
|
//
|
|
aData = [thePassword dataUsingEncoding: NSISOLatin1StringEncoding];
|
|
|
|
[strongSelf sendCommand: IMAP_LOGIN
|
|
info: [NSDictionary dictionaryWithObject: aData forKey: @"Password"]
|
|
arguments: @"LOGIN %@ {%d}", strongSelf->_username, [aData length]];
|
|
return;
|
|
}
|
|
|
|
[strongSelf sendCommand: IMAP_LOGIN
|
|
info: nil
|
|
arguments: @"LOGIN %@ %@", strongSelf->_username, safePassword];
|
|
});
|
|
}
|
|
|
|
#pragma mark - CWStore
|
|
|
|
//
|
|
// Create the mailbox and subscribe to it. The full path to the mailbox must
|
|
// be provided.
|
|
//
|
|
// The delegate will be notified when the folder has been created (or not).
|
|
//
|
|
- (void) createFolderWithName: (NSString *) theName
|
|
type: (PantomimeFolderFormat) theType
|
|
contents: (NSData *) theContents
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[self sendCommand: IMAP_CREATE
|
|
info: [NSDictionary dictionaryWithObject: theName forKey: @"Name"]
|
|
arguments: @"CREATE \"%@\"", [theName modifiedUTF7String]];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// Delete the mailbox. The full path to the mailbox must be provided.
|
|
//
|
|
// The delegate will be notified when the folder has been deleted (or not).
|
|
//
|
|
- (void) deleteFolderWithName: (NSString *) theName
|
|
{
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
[self sendCommand: IMAP_DELETE
|
|
info: [NSDictionary dictionaryWithObject: theName forKey: @"Name"]
|
|
arguments: @"DELETE \"%@\"", [theName modifiedUTF7String]];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// This method is used to rename a folder.
|
|
//
|
|
// theName and theNewName MUST be the full path of those mailboxes.
|
|
// If they begin with the folder separator (ie., '/'), the character is
|
|
// automatically stripped.
|
|
//
|
|
// This method supports renaming SELECT'ed mailboxes.
|
|
//
|
|
// The delegate will be notified when the folder has been renamed (or not).
|
|
//
|
|
- (void) renameFolderWithName: (NSString *) theName
|
|
toName: (NSString *) theNewName
|
|
{
|
|
__block NSString *blockName = theName;
|
|
__block NSString *blockNewName = theNewName;
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
NSDictionary *info;
|
|
|
|
blockName = [blockName stringByDeletingFirstPathSeparator: strongSelf->_folderSeparator];
|
|
blockNewName = [blockNewName stringByDeletingFirstPathSeparator: strongSelf->_folderSeparator];
|
|
info = [NSDictionary dictionaryWithObjectsAndKeys: blockName, @"Name", blockNewName, @"NewName", nil];
|
|
|
|
if ([[blockName stringByTrimmingWhiteSpaces] length] == 0 ||
|
|
[[blockNewName stringByTrimmingWhiteSpaces] length] == 0)
|
|
{
|
|
PERFORM_SELECTOR_3(strongSelf->_delegate, @selector(folderRenameFailed:),
|
|
PantomimeFolderRenameFailed, info);
|
|
}
|
|
|
|
[strongSelf sendCommand: IMAP_RENAME
|
|
info: info
|
|
arguments: @"RENAME \"%@\" \"%@\"", [blockName modifiedUTF7String],
|
|
[blockNewName modifiedUTF7String]];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void)listFolders
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
|
|
// Throw away cached info about folders, and always fetch from server
|
|
strongSelf->_folders = [[NSMutableDictionary alloc] init];
|
|
|
|
// Only top level folders: LIST "" %
|
|
[strongSelf sendCommand: IMAP_LIST info: nil arguments: @"LIST \"\" *"];
|
|
});
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void)sendIdle
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
[strongSelf sendCommand: IMAP_IDLE info: nil arguments: @"IDLE"];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
// This method works the same way as the -folderEnumerator method.
|
|
//
|
|
- (NSEnumerator *) subscribedFolderEnumerator
|
|
{
|
|
__block NSEnumerator *returnee = nil;
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
if (![strongSelf->_subscribedFolders count])
|
|
{
|
|
[strongSelf sendCommand: IMAP_LSUB info: nil arguments: @"LSUB \"\" \"*\""];
|
|
returnee = nil;
|
|
|
|
return;
|
|
}
|
|
|
|
returnee = [strongSelf->_subscribedFolders objectEnumerator];
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (id) folderForURL: (NSString *) theURL
|
|
{
|
|
__block id returnee = nil;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
CWURLName *theURLName;
|
|
id aFolder;
|
|
|
|
theURLName = [[CWURLName alloc] initWithString: theURL];
|
|
|
|
aFolder = [self folderForNameInternal: [theURLName foldername]];
|
|
RELEASE(theURLName);
|
|
returnee = aFolder;
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (NSEnumerator *) openFoldersEnumerator
|
|
{
|
|
__block NSEnumerator *returnee = nil;
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
returnee = [strongSelf->_openFolders objectEnumerator];
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) removeFolderFromOpenFolders: (CWFolder *) theFolder
|
|
{
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
if (strongSelf->_selectedFolder == (CWIMAPFolder *)theFolder)
|
|
{
|
|
strongSelf->_selectedFolder = nil;
|
|
}
|
|
|
|
[strongSelf->_openFolders removeObjectForKey: [theFolder name]];
|
|
});
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (BOOL) folderForNameIsOpen: (NSString *) theName
|
|
{
|
|
__block BOOL returnee = NO;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
NSEnumerator *anEnumerator;
|
|
CWIMAPFolder *aFolder;
|
|
|
|
anEnumerator = [self openFoldersEnumerator];
|
|
|
|
while ((aFolder = [anEnumerator nextObject]))
|
|
{
|
|
if ([[aFolder name] compare: theName
|
|
options: NSCaseInsensitiveSearch] == NSOrderedSame)
|
|
{
|
|
returnee = YES;
|
|
return;
|
|
}
|
|
}
|
|
|
|
returnee = NO;
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
|
|
//
|
|
// This method verifies in the cache if theName is present.
|
|
// If so, it returns the associated value.
|
|
//
|
|
// If it's not present, it sends a LIST command to the server
|
|
// and the delegate will eventually be notified when the LIST
|
|
// command completed. It also returns 0 if it's not present.
|
|
//
|
|
- (PantomimeFolderAttribute) folderTypeForFolderName: (NSString *) theName
|
|
{
|
|
__block PantomimeFolderAttribute returnee = 0;
|
|
__weak typeof(self) weakSelf = self;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
id o = [strongSelf->_folders objectForKey: theName];
|
|
|
|
if (o)
|
|
{
|
|
returnee = [o intValue];
|
|
return;
|
|
}
|
|
|
|
[strongSelf sendCommand: IMAP_LIST
|
|
info: nil
|
|
arguments: @"LIST \"\" \"%@\"", [theName modifiedUTF7String]];
|
|
});
|
|
|
|
return returnee;
|
|
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (unsigned char) folderSeparator
|
|
{
|
|
@synchronized (self) {
|
|
return _folderSeparator;
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// The default folder in IMAP is always Inbox. This method will fetch
|
|
// the messages of an IMAP folder if they haven't been fetched before.
|
|
//
|
|
- (id) defaultFolder
|
|
{
|
|
// is serialized by folderForName:
|
|
return [self folderForName: @"INBOX" updateExistsCount: NO];
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (CWIMAPFolder *)folderWithName:(NSString *)name
|
|
{
|
|
NSAssert(NO, @"Overwrite CWIMAPStore.folderWithName");
|
|
return nil;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (id) folderForName: (NSString *) theName updateExistsCount: (BOOL)updateExistsCount
|
|
{
|
|
__block id returnee = nil;
|
|
dispatch_sync(self.serviceQueue, ^{
|
|
returnee = [self folderForNameInternal: theName
|
|
mode: PantomimeReadWriteMode
|
|
updateExistsCount:updateExistsCount];
|
|
});
|
|
|
|
return returnee;
|
|
}
|
|
|
|
//
|
|
//
|
|
// Non-serialized helper to avoid deadlocks chaining folderForName:... methods
|
|
- (id) folderForNameInternal: (NSString *) theName
|
|
{
|
|
return [self folderForNameInternal: theName
|
|
mode: PantomimeReadWriteMode updateExistsCount:NO];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
//
|
|
// Private methods
|
|
//
|
|
@implementation CWIMAPStore (Private)
|
|
|
|
//
|
|
// This method is used to parse the name of a mailbox.
|
|
//
|
|
// If the string was encoded using mUTF-7, it'll also
|
|
// decode it.
|
|
//
|
|
- (NSString *) _folderNameFromString: (NSString *) theString
|
|
{
|
|
NSString *aString, *decodedString;
|
|
NSRange aRange;
|
|
|
|
aRange = [theString rangeOfString: @"\""];
|
|
|
|
if (aRange.length)
|
|
{
|
|
NSUInteger mark;
|
|
|
|
mark = aRange.location + 1;
|
|
|
|
aRange = [theString rangeOfString: @"\""
|
|
options: 0
|
|
range: NSMakeRange(mark, [theString length] - mark)];
|
|
|
|
aString = [theString substringWithRange: NSMakeRange(mark, aRange.location - mark)];
|
|
|
|
// Check if we got "NIL" or a real separator.
|
|
if ([aString length] == 1)
|
|
{
|
|
_folderSeparator = [aString characterAtIndex: 0];
|
|
}
|
|
else
|
|
{
|
|
_folderSeparator = 0;
|
|
}
|
|
|
|
mark = aRange.location + 2;
|
|
aString = [theString substringFromIndex: mark];
|
|
}
|
|
else
|
|
{
|
|
aRange = [theString rangeOfString: @"NIL" options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.length)
|
|
{
|
|
aString = [theString substringFromIndex: aRange.location + aRange.length + 1];
|
|
}
|
|
else
|
|
{
|
|
return theString;
|
|
}
|
|
}
|
|
|
|
aString = [aString stringFromQuotedString];
|
|
decodedString = [aString stringFromModifiedUTF7];
|
|
|
|
return (decodedString != nil ? decodedString : aString);
|
|
}
|
|
|
|
|
|
//
|
|
// This method parses the flags received in theString and builds
|
|
// a corresponding Flags object for them.
|
|
//
|
|
- (void) _parseFlags: (NSString *) theString
|
|
message: (CWIMAPMessage *) theMessage
|
|
record: (CWCacheRecord *) theRecord
|
|
{
|
|
CWFlags *theFlags;
|
|
NSRange aRange;
|
|
|
|
theFlags = [[CWFlags alloc] init];
|
|
|
|
// We check if the message has the Seen flag
|
|
aRange = [theString rangeOfString: @"\\Seen"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagSeen];
|
|
}
|
|
|
|
// We check if the message has the Recent flag
|
|
aRange = [theString rangeOfString: @"\\Recent"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagRecent];
|
|
}
|
|
|
|
// We check if the message has the Deleted flag
|
|
aRange = [theString rangeOfString: @"\\Deleted"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagDeleted];
|
|
}
|
|
|
|
// We check if the message has the Answered flag
|
|
aRange = [theString rangeOfString: @"\\Answered"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagAnswered];
|
|
}
|
|
|
|
// We check if the message has the Flagged flag
|
|
aRange = [theString rangeOfString: @"\\Flagged"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagFlagged];
|
|
}
|
|
|
|
// We check if the message has the Draft flag
|
|
aRange = [theString rangeOfString: @"\\Draft"
|
|
options: NSCaseInsensitiveSearch];
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[theFlags add: PantomimeFlagDraft];
|
|
}
|
|
|
|
[[theMessage flags] replaceWithFlags: theFlags];
|
|
theRecord.flags = theFlags->flags;
|
|
RELEASE(theFlags);
|
|
|
|
//
|
|
// If our previous command is NOT the FETCH command, we must inform our
|
|
// delegate that messages flags have changed. The delegate SHOULD refresh
|
|
// its view and does NOT have to issue any command to update the state
|
|
// of the messages (since it has been done).
|
|
//
|
|
if (_lastCommand != IMAP_UID_FETCH_BODY_TEXT && _lastCommand != IMAP_UID_FETCH_HEADER_FIELDS &&
|
|
_lastCommand != IMAP_UID_FETCH_HEADER_FIELDS_NOT && _lastCommand != IMAP_UID_FETCH_RFC822)
|
|
{
|
|
NSDictionary *userInfo = [NSDictionary
|
|
dictionaryWithObject: theMessage forKey: @"Message"];
|
|
PERFORM_SELECTOR_2(_delegate, @selector(messageChanged:), PantomimeMessageChanged,
|
|
userInfo, PantomimeMessageChanged);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _renameFolder
|
|
{
|
|
CWFolderInformation *aFolderInformation;
|
|
NSString *aName, *aNewName;
|
|
CWIMAPFolder *aFolder;
|
|
|
|
aName = [self.currentQueueObject.info objectForKey: @"Name"];
|
|
aNewName = [self.currentQueueObject.info objectForKey: @"NewName"];
|
|
|
|
// If the folder was open, we change its name and recache its entry.
|
|
aFolder = [_openFolders objectForKey: aName];
|
|
|
|
if (aFolder)
|
|
{
|
|
RETAIN_VOID(aFolder);
|
|
[aFolder setName: aNewName];
|
|
[_openFolders removeObjectForKey: aName];
|
|
[_openFolders setObject: aFolder forKey: aNewName];
|
|
RELEASE(aFolder);
|
|
}
|
|
|
|
// We then do the same thing for our list of folders / suscribed folders
|
|
aFolderInformation = RETAIN([_folders objectForKey: aName]);
|
|
[_folders removeObjectForKey: aName];
|
|
|
|
if (aFolderInformation)
|
|
{
|
|
[_folders setObject: aFolderInformation forKey: aNewName];
|
|
RELEASE(aFolderInformation);
|
|
}
|
|
|
|
if ([_subscribedFolders containsObject: aName])
|
|
{
|
|
[_subscribedFolders removeObject: aName];
|
|
[_subscribedFolders addObject: aNewName];
|
|
}
|
|
}
|
|
|
|
//
|
|
// This method parses a IMAP_UID_FETCH_UIDS response in order to decode
|
|
// all UIDs in the result.
|
|
//
|
|
// Examples:
|
|
// "* 170 FETCH (UID 95516)"
|
|
//
|
|
- (NSArray<NSNumber*> *)_uniqueIdentifiersFromFetchUidsResponseData:(NSData *)response
|
|
{
|
|
NSString *searchResponsePrefix = @"* X FETCH";
|
|
return [self _uniqueIdentifiersFromData: response
|
|
skippingFirstNumberOfChars: searchResponsePrefix.length];
|
|
}
|
|
|
|
//
|
|
// This method parses a SEARCH response in order to decode
|
|
// all UIDs in the result.
|
|
//
|
|
// Examples of theData:
|
|
//
|
|
// "* SEARCH 1 4 59 81"
|
|
// "* SEARCH"
|
|
//
|
|
- (NSArray<NSNumber*> *)_uniqueIdentifiersFromSearchResponseData:(NSData *)response
|
|
{
|
|
NSString *searchResponsePrefix = @"* SEARCH";
|
|
return [self _uniqueIdentifiersFromData: response
|
|
skippingFirstNumberOfChars: searchResponsePrefix.length];
|
|
}
|
|
|
|
|
|
- (NSArray<NSNumber*> *)_uniqueIdentifiersFromData:(NSData *)theData
|
|
skippingFirstNumberOfChars:(NSUInteger)numSkipPre
|
|
{
|
|
NSMutableArray<NSNumber*> *results = [NSMutableArray new];
|
|
if (numSkipPre >= theData.length) {
|
|
// Nothing to scan.
|
|
return results;
|
|
}
|
|
theData = [theData subdataFromIndex: numSkipPre];
|
|
if (![theData length]) {
|
|
// Nothing to scan.
|
|
return results;
|
|
}
|
|
|
|
// We scan all our UIDs.
|
|
NSScanner *scanner = [[NSScanner alloc] initWithString: [theData asciiString]];
|
|
NSUInteger value = 0;
|
|
while (![scanner isAtEnd]) {
|
|
[scanner scanUnsignedInt: &value];
|
|
if (value != 0) {
|
|
[results addObject: [NSNumber numberWithInteger: value]];
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseAUTHENTICATE_CRAM_MD5
|
|
{
|
|
NSData *aData;
|
|
|
|
aData = [_responsesFromServer lastObject];
|
|
|
|
//
|
|
// We first verify if we got our challenge response from the IMAP server.
|
|
// If so, we use it and send back a response to proceed with the authentication.
|
|
//
|
|
if ([aData hasCPrefix: "+"])
|
|
{
|
|
NSString *aString;
|
|
CWMD5 *aMD5;
|
|
|
|
// We trim the "+ " and we keep the challenge phrase
|
|
aData = [aData subdataFromIndex: 2];
|
|
|
|
//LogInfo(@"Challenge phrase = |%@|", [aData asciiString]);
|
|
aMD5 = [[CWMD5 alloc] initWithData: [aData decodeBase64]];
|
|
[aMD5 computeDigest];
|
|
|
|
aString = [NSString stringWithFormat: @"%@ %@", _username, [aMD5 hmacAsStringUsingPassword: _password]];
|
|
aString = [[NSString alloc] initWithData: [[aString dataUsingEncoding: NSASCIIStringEncoding] encodeBase64WithLineLength: 0]
|
|
encoding: NSASCIIStringEncoding];
|
|
|
|
[self bulkWriteData:@[[aString dataUsingEncoding: _defaultCStringEncoding],
|
|
_crlf]];
|
|
RELEASE(aMD5);
|
|
RELEASE(aString);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// LOGIN is a very lame authentication method but we support it anyway. We basically
|
|
// wait for a challenge, send the username (in base64), wait for an other challenge
|
|
// and finally send the password (in base64). The challenges aren't even used.
|
|
//
|
|
- (void) _parseAUTHENTICATE_LOGIN
|
|
{
|
|
NSData *aData;
|
|
|
|
aData = [_responsesFromServer lastObject];
|
|
|
|
//
|
|
// We first verify if we got our challenge response from the IMAP server.
|
|
// If so, we use it and send back a response to proceed with the authentication.
|
|
// Based on what we sent before, we can either send the username or the password.
|
|
//
|
|
if ([aData hasCPrefix: "+"])
|
|
{
|
|
NSData *aResponse;
|
|
|
|
// Have we read the initial challenge? If not, we must send the username!
|
|
if (self.currentQueueObject && ![self.currentQueueObject.info
|
|
objectForKey: @"Challenge"])
|
|
{
|
|
aResponse = [[_username dataUsingEncoding: NSASCIIStringEncoding]
|
|
encodeBase64WithLineLength: 0];
|
|
[self.currentQueueObject.info setObject: aData forKey: @"Challenge"];
|
|
}
|
|
else
|
|
{
|
|
aResponse = [[_password dataUsingEncoding: NSASCIIStringEncoding]
|
|
encodeBase64WithLineLength: 0];
|
|
}
|
|
|
|
[self bulkWriteData:@[aResponse, _crlf]];
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseBAD
|
|
{
|
|
// Synchronize all methods that alter the _queue
|
|
@synchronized(self) {
|
|
NSData *aData;
|
|
|
|
aData = [_responsesFromServer lastObject];
|
|
|
|
LogError(@"IN _parseBAD: |%@| %d", [aData asciiString], _lastCommand);
|
|
|
|
switch (_lastCommand)
|
|
{
|
|
case IMAP_LOGIN:
|
|
// This can happen if we got an empty username or password.
|
|
AUTHENTICATION_FAILED(_delegate, _mechanism);
|
|
break;
|
|
case IMAP_AUTHENTICATE_CRAM_MD5:
|
|
case IMAP_AUTHENTICATE_XOAUTH2: // Only added for completenes. In reality XOAuth2 never responds with BAD (only Gmail tested so far)
|
|
case IMAP_AUTHENTICATE_LOGIN:
|
|
// Probably wrong credentials.
|
|
// Example case: 0003 BAD [AUTHENTICATIONFAILED] AUTHENTICATE Invalid credentials
|
|
AUTHENTICATION_FAILED(_delegate, _mechanism);
|
|
break;
|
|
case IMAP_SELECT: {
|
|
[_queue removeLastObject];
|
|
[_responsesFromServer removeAllObjects];
|
|
|
|
if ([_selectedFolder.name isEqualToString:PantomimeFolderNameToIgnore]) {
|
|
// PantomimeFolderNameToIgnore is used as a workaround to close a mailbox (aka. folder)
|
|
// without calling CLOSE.
|
|
// see RFC4549-4.2.5
|
|
_selectedFolder = nil;
|
|
PERFORM_SELECTOR_2([self delegate], @selector(folderCloseCompleted:), PantomimeFolderCloseCompleted, self, @"Folder");
|
|
return;
|
|
}
|
|
|
|
NSDictionary *userInfo = @{PantomimeBadResponseInfoKey:[aData asciiString]};
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderOpenFailed:),
|
|
PantomimeFolderOpenFailed, userInfo,
|
|
PantomimeErrorInfo);
|
|
}
|
|
break;
|
|
case IMAP_UID_MOVE:
|
|
default:
|
|
// We got a BAD response that we could not handle. Inform the delegate,
|
|
// post a notification and remove the command that caused this from the queue.
|
|
[_queue removeLastObject];
|
|
[_responsesFromServer removeAllObjects];
|
|
|
|
NSDictionary *userInfo = @{PantomimeBadResponseInfoKey: [aData asciiString]};
|
|
PERFORM_SELECTOR_2(_delegate, @selector(badResponse:),
|
|
PantomimeBadResponse, userInfo,
|
|
PantomimeErrorInfo);
|
|
}
|
|
|
|
if (![aData hasCPrefix: "*"])
|
|
{
|
|
[_queue removeLastObject];
|
|
[self sendCommand: IMAP_EMPTY_QUEUE info: nil arguments: @""];
|
|
}
|
|
|
|
[_responsesFromServer removeAllObjects];
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseBYE
|
|
{
|
|
//
|
|
// We check if we sent the IMAP_LOGOUT command.
|
|
//
|
|
// If we got an untagged BYE response, it means
|
|
// that the server disconnected us. We will
|
|
// handle that in CWService: -updateRead.
|
|
//
|
|
if (_lastCommand == IMAP_LOGOUT)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// This method parses an * CAPABILITY IMAP4 IMAP4rev1 ACL AUTH=LOGIN NAMESPACE ..
|
|
// untagged response (6.1.1)
|
|
- (void) _parseCAPABILITY
|
|
{
|
|
NSString *aString;
|
|
NSData *aData;
|
|
|
|
aData = [_responsesFromServer objectAtIndex: 0];
|
|
aString = [[NSString alloc] initWithData: aData encoding: _defaultCStringEncoding];
|
|
|
|
[_capabilities addObjectsFromArray: [[aString substringFromIndex: 13] componentsSeparatedByString: @" "]];
|
|
RELEASE(aString);
|
|
|
|
if (_connection_state.reconnecting)
|
|
{
|
|
[self authenticate: _username password: _password mechanism: _mechanism];
|
|
}
|
|
else
|
|
{
|
|
PERFORM_SELECTOR_1(_delegate, @selector(serviceInitialized:), PantomimeServiceInitialized);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// This method parses an * 23 EXISTS untagged response. (7.3.1)
|
|
//
|
|
// If we were NOT issueing a SELECT command, it informs the folder's delegate that
|
|
// new messages have arrived.
|
|
//
|
|
- (void) _parseEXISTS
|
|
{
|
|
NSData *aData;
|
|
int n;
|
|
|
|
aData = [_responsesFromServer lastObject];
|
|
sscanf([aData cString], "* %d EXISTS", &n);
|
|
_selectedFolder.existsCount = n;
|
|
LogInfo(@"EXISTS %d", n);
|
|
if (_lastCommand == IMAP_IDLE) {
|
|
PERFORM_SELECTOR_1(_delegate, @selector(idleNewMessages:), PantomimeIdleNewMessages);
|
|
}
|
|
}
|
|
|
|
#pragma mark - --
|
|
|
|
//
|
|
// Example: * 44 EXPUNGE
|
|
//
|
|
- (void)_parseEXPUNGE
|
|
{
|
|
if (_lastCommand == IMAP_UID_STORE) {
|
|
// Calling IMAP_UID_STORE to set a \deleted flag on Gmail server, the server moves those
|
|
// messages to "All Messages", expunges it from the folder it has been deleted in and
|
|
// reports it here.
|
|
// The client has to take care to delete the original msg currently.
|
|
return;
|
|
}
|
|
|
|
int msn;
|
|
NSData *aData = [_responsesFromServer lastObject];
|
|
sscanf([aData cString], "* %d EXPUNGE", &msn);
|
|
|
|
// It looks like some servers send untagged expunge reponses
|
|
// _after_ the selected folder has been closed.
|
|
if (!_selectedFolder) {
|
|
LogInfo(@"EXPUNGE %d on already closed folder", msn);
|
|
return;
|
|
}
|
|
// The conditions for being able to react safely to expunges have to be verified.
|
|
// In the case of IDLE, it's probably safe.
|
|
if (_lastCommand != IMAP_IDLE && _lastCommand != IMAP_UID_MOVE) {
|
|
return;
|
|
}
|
|
|
|
//LogInfo(@"EXPUNGE %d", msn);
|
|
|
|
// Messages CAN be expunged before we really had time to FETCH them.
|
|
// We simply proceed by skipping over MSN that are bigger than we
|
|
// we have so far. It should be safe since the view hasn't even
|
|
// had the chance to display them.
|
|
if (msn > [_selectedFolder lastMSN]) {
|
|
return;
|
|
}
|
|
|
|
CWIMAPMessage *aMessage = (CWIMAPMessage *) [_selectedFolder messageAtIndex: msn];
|
|
|
|
//!!!: The following is a complete mess. Inconsistant. Wrong.
|
|
//!!!: Please rethink, rewrite and remove.
|
|
|
|
// We do NOT use [_selectedFolder removeMessage: aMessage] since it'll
|
|
// thread the messages everytime we invoke it. We rather thread messages
|
|
// if:
|
|
// * We got an untagged EXPUNGE response but the last command was NOT
|
|
// an EXPUNGE one (see below, near the end of the method)
|
|
// * We sent an EXPUNGE command - we'll do the threading of the
|
|
// messages in _parseOK:
|
|
[_selectedFolder removeMessage: aMessage]; // responsible for also shifting following MSNs
|
|
[_selectedFolder updateCache];
|
|
|
|
// We remove its entry in our cache
|
|
if ([_selectedFolder cacheManager]) {
|
|
[(CWIMAPCacheManager *)[_selectedFolder cacheManager] removeMessageWithUID: [aMessage UID]]; //This doubles remove Message above. Cleanup cacheManager mess.
|
|
}
|
|
|
|
// Keep exist count up to date.
|
|
_selectedFolder.existsCount = _selectedFolder.existsCount - 1;
|
|
|
|
// If our previous command is NOT the EXPUNGE command, we must inform our
|
|
// delegate that messages have been expunged. The delegate SHOULD refresh
|
|
// its view and does NOT have to issue any command to update the state
|
|
// of the messages (since it has been done).
|
|
if (_lastCommand != IMAP_EXPUNGE) {
|
|
PERFORM_SELECTOR_1(_delegate, @selector(messageExpunged:), PantomimeMessageExpunged);
|
|
}
|
|
LogInfo(@"Expunged %d", msn);
|
|
}
|
|
|
|
/**
|
|
@Return: The UID extracted from a list of NSData (as part of a fetch request), or
|
|
0 if none could be identified.
|
|
*/
|
|
- (NSUInteger)extractUIDFromDataArray:(NSArray *)datas
|
|
{
|
|
for (NSData *data in datas) {
|
|
NSString *aString = [data asciiString];
|
|
NSTextCheckingResult *match = [self.uidRegex
|
|
firstMatchInString:aString options:0
|
|
range:NSMakeRange(0, aString.length)];
|
|
if (match) {
|
|
NSRange r = [match rangeAtIndex:1];
|
|
if (r.location != NSNotFound) {
|
|
NSString *uidString = [aString substringWithRange:r];
|
|
return uidString.integerValue;
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
for (NSData *data in datas) {
|
|
NSString *aString = [data asciiString];
|
|
LogInfo("extractUID: '%@'", aString);
|
|
}
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
Examples:
|
|
"* 170 FETCH (UID 95516)"
|
|
*/
|
|
- (void) _parseFETCH_UIDS
|
|
{
|
|
NSArray<NSNumber*> *uidsFromResponse =
|
|
[self _uniqueIdentifiersFromFetchUidsResponseData:[_responsesFromServer lastObject]];
|
|
|
|
LogInfo(@"[_responsesFromServer lastObject]: %@",
|
|
[[_responsesFromServer lastObject] asciiString]);
|
|
LogInfo(@"uidsFromResponse: %@", uidsFromResponse);
|
|
|
|
NSArray *alreadyParsedUids = self.currentQueueObject.info[@"Uids"];
|
|
LogInfo(@"alreadyParsedUids: %@", alreadyParsedUids);
|
|
if (!alreadyParsedUids) {
|
|
alreadyParsedUids = uidsFromResponse;
|
|
} else {
|
|
alreadyParsedUids = [alreadyParsedUids arrayByAddingObjectsFromArray:uidsFromResponse];
|
|
}
|
|
// Store/update the results in our command queue.
|
|
[self.currentQueueObject.info setObject:alreadyParsedUids forKey:@"Uids"];
|
|
}
|
|
|
|
//
|
|
//
|
|
// Examples of FETCH responses:
|
|
//
|
|
// * 50 FETCH (UID 50 RFC822 {6718}
|
|
// Return-Path: <...
|
|
// )
|
|
//
|
|
//
|
|
// * 418 FETCH (FLAGS (\Seen) UID 418 RFC822.SIZE 3565452 BODY[HEADER.FIELDS (From To Cc Subject Date Message-ID
|
|
// References In-Reply-To MIME-Version)] {666}
|
|
// Subject: abc
|
|
// ...
|
|
// )
|
|
//
|
|
//
|
|
// * 50 FETCH (UID 50 BODY[HEADER.FIELDS.NOT (From To Cc Subject Date Message-ID References In-Reply-To MIME-Version)] {1412}
|
|
// Return-Path: <...
|
|
// )
|
|
//
|
|
// * 50 FETCH (BODY[TEXT] {5009}
|
|
// Hi, ...
|
|
// )
|
|
//
|
|
//
|
|
// "Twisted" response from Microsoft Exchange 2000:
|
|
//
|
|
// * 549 FETCH (FLAGS (\Recent) RFC822.SIZE 970 BODY[HEADER.FIELDS (From To Cc Subject Date Message-ID References In-Reply-To MIME-Version)] {196}
|
|
// From: <aaaaaa@bbbbbbbbbbbbbbb.com>
|
|
// To: aaaaaa@bbbbbbbbbbbbbbb.com
|
|
// Subject: Test mail
|
|
// Date: Tue, 16 Dec 2003 15:52:23 GMT
|
|
// Message-Id: <200312161552.PAA07523@aaaaaaa.bbb.ccccccccccccccc.com>
|
|
//
|
|
// UID 29905)
|
|
//
|
|
//
|
|
// Yet an other "twisted" response, likely coming from UW IMAP Server (2001.315rh)
|
|
//
|
|
// * 741 FETCH (UID 23628 BODY[TEXT] {818}
|
|
// f00bar baz
|
|
// ...
|
|
// )
|
|
// * 741 FETCH (FLAGS (\Seen) UID 23628)
|
|
// 000b OK UID FETCH completed
|
|
//
|
|
//
|
|
// Other examples:
|
|
//
|
|
// * 1 FETCH (FLAGS (\Seen) UID 97 RFC822.SIZE 19123 BODY[HEADER.FIELDS (From To Cc Subject Date
|
|
// Message-ID References In-Reply-To MIME-Version)] {216}
|
|
//
|
|
// This method can be called more than on times for a message. For example, Exchange sends
|
|
// answers like this one:
|
|
//
|
|
// * 9 FETCH (BODY[HEADER.FIELDS.NOT (From To Cc Subject Date Message-ID References In-Reply-To MIME-Version)] {408}
|
|
// Received: by nt1.inverse.qc.ca
|
|
// .id <01C34ADC.D13E2A20@nt1.inverse.qc.ca>; Tue, 15 Jul 2003 09:24:36 -0500
|
|
// content-class: urn:content-classes:message
|
|
// Content-Type: multipart/mixed;
|
|
// .boundary="----_=_NextPart_001_01C34ADC.D13E2A20"
|
|
// X-MS-Has-Attach: yes
|
|
// X-MS-TNEF-Correlator:
|
|
// Thread-Topic: test5
|
|
// X-MimeOLE: Produced By Microsoft Exchange V6.0.6249.0
|
|
// Thread-Index: AcNK3NDIIAS/1aRKSYC4x2N4Zj3GGg==
|
|
//
|
|
// UID 9)
|
|
//
|
|
// And we MUST parse the UID correctly.
|
|
//
|
|
- (void) _parseFETCH: (NSInteger) theMSN
|
|
{
|
|
NSMutableString *aMutableString;
|
|
NSCharacterSet *aCharacterSet;
|
|
CWIMAPMessage *aMessage;
|
|
NSScanner *aScanner;
|
|
|
|
NSMutableArray *aMutableArray;
|
|
NSString *aWord, *aString;
|
|
NSRange aRange;
|
|
|
|
BOOL done, seen_fetch, must_flush_record;
|
|
// Indicates whether we are creating a new mail or are updating an existing one
|
|
BOOL isMessageUpdate = NO;
|
|
NSInteger i, j, count, len;
|
|
CWCacheRecord *cacheRecord = [[CWCacheRecord alloc] init];
|
|
|
|
//
|
|
// The folder might have been closed so we must not try to
|
|
// update it for no good reason.
|
|
//
|
|
if (!_selectedFolder) return;
|
|
|
|
if (!_selectedFolder.selected) {
|
|
[NSException raise: PantomimeProtocolException
|
|
format: @"Unable to fetch message content from unselected mailbox."];
|
|
}
|
|
|
|
count = [_responsesFromServer count]-1;
|
|
|
|
//LogInfo(@"RESPONSES FROM SERVER: %d", count);
|
|
|
|
aMutableString = [[NSMutableString alloc] init];
|
|
aMutableArray = [[NSMutableArray alloc] init];
|
|
|
|
//
|
|
// Note:
|
|
//
|
|
// We must be careful here to NOT consider all responses from the server. For example,
|
|
// UW IMAP might send us:
|
|
// 1 UID SEARCH ANSWERED
|
|
// * SEARCH
|
|
// * 1 FETCH (FLAGS (\Recent \Seen) UID 1)
|
|
// 1 OK UID SEARCH completed
|
|
//
|
|
// In such response, we must NOT consider the "* SEARCH" response.
|
|
//
|
|
must_flush_record = seen_fetch = NO;
|
|
|
|
// Extract the UID from anywhere in the response
|
|
NSUInteger theUID = [self extractUIDFromDataArray:_responsesFromServer.array];
|
|
|
|
if (theUID == 0) {
|
|
// If there is no UID in this response, try to deduce it from the mapping
|
|
theUID = [_selectedFolder uidForMSN:theMSN];
|
|
}
|
|
|
|
LogInfo(@"parseFETCH theMSN %lu, UID %lu", (unsigned long) theMSN, (unsigned long)theUID);
|
|
|
|
// Try to retrieve the message by UID
|
|
if (theUID > 0) {
|
|
LogInfo(@"Trying existing message for UID %lu", (unsigned long)theUID);
|
|
aMessage = (CWIMAPMessage *) [_selectedFolder.cacheManager messageWithUID:theUID];
|
|
}
|
|
|
|
if (aMessage == nil) {
|
|
LogInfo(@"New message");
|
|
aMessage = [[CWIMAPMessage alloc] init];
|
|
// We set some initial properties to our message;
|
|
[aMessage setInitialized: NO];
|
|
[aMessage setFolder: _selectedFolder];
|
|
[_selectedFolder appendMessage: aMessage];
|
|
} else {
|
|
isMessageUpdate = YES;
|
|
}
|
|
|
|
CWMessageUpdate *messageUpdate = [CWMessageUpdate new];
|
|
|
|
if (!isMessageUpdate) {
|
|
// A UID must never change for an existing mail
|
|
[aMessage setUID:theUID];
|
|
messageUpdate.uid = YES;
|
|
}
|
|
|
|
// We add the new message to our cache.
|
|
if ([_selectedFolder cacheManager]) {
|
|
if (must_flush_record)
|
|
{
|
|
[[_selectedFolder cacheManager] writeRecord: cacheRecord message: aMessage]; //This can never be reached afaics. Check, remove.
|
|
}
|
|
|
|
CLEAR_CACHE_RECORD(cacheRecord);
|
|
must_flush_record = YES;
|
|
//[[_selectedFolder cacheManager] addObject: aMessage];
|
|
}
|
|
|
|
for (i = 0; i <= count; i++) {
|
|
aString = [[_responsesFromServer objectAtIndex: i] asciiString];
|
|
|
|
//LogInfo(@"%i: %@", i, aString);
|
|
if (!seen_fetch && [aString hasCaseInsensitivePrefix: [NSString stringWithFormat: @"* %ld FETCH", (long)theMSN]])
|
|
{
|
|
seen_fetch = YES;
|
|
}
|
|
|
|
if (seen_fetch) {
|
|
[aMutableArray addObject: [_responsesFromServer objectAtIndex: i]];
|
|
[aMutableString appendString: aString];
|
|
if (i < count-1) {
|
|
[aMutableString appendString: @" "];
|
|
}
|
|
}
|
|
}
|
|
|
|
//LogInfo(@"GOT TO PARSE: |%@|", aMutableString);
|
|
|
|
aCharacterSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
|
|
len = [aMutableString length];
|
|
i = j = 0;
|
|
|
|
aScanner = [[NSScanner alloc] initWithString: aMutableString];
|
|
[aScanner setScanLocation: i];
|
|
|
|
done = ![aScanner scanUpToCharactersFromSet: aCharacterSet intoString: NULL];
|
|
|
|
//
|
|
// We tokenize our string into words
|
|
//
|
|
while (!done) {
|
|
j = [aScanner scanLocation];
|
|
aWord = [[aMutableString substringWithRange: NSMakeRange(i,j-i)] stringByTrimmingWhiteSpaces];
|
|
|
|
//LogInfo(@"WORD |%@|", aWord);
|
|
|
|
if ([aWord characterAtIndex: 0] == '(') {
|
|
aWord = [aWord substringFromIndex: 1];
|
|
}
|
|
|
|
//
|
|
// We read the MSN
|
|
//
|
|
if ([aWord characterAtIndex: 0] == '*') {
|
|
int msn;
|
|
|
|
[aScanner scanInt: &msn];
|
|
//LogInfo(@"*** msn = %d", msn);
|
|
if (aMessage.messageNumber != msn) {
|
|
[aMessage setMessageNumber: msn];
|
|
messageUpdate.msn = YES;
|
|
}
|
|
|
|
// Store any mapping MSN -> UID that came from the server
|
|
[_selectedFolder matchUID:theUID withMSN:theMSN];
|
|
}
|
|
// end of reading MSN
|
|
|
|
//
|
|
// We read our UID
|
|
//
|
|
if ([aWord caseInsensitiveCompare: @"UID"] == NSOrderedSame)
|
|
{
|
|
NSUInteger uid;
|
|
|
|
[aScanner scanUnsignedInt: &uid];
|
|
//LogInfo(@"uid %d j = %d, scanLoc = %d", uid, j, [aScanner scanLocation]);
|
|
|
|
if ([aMessage UID] == 0)
|
|
{
|
|
[aMessage setUID: uid];
|
|
cacheRecord.imap_uid = uid;
|
|
messageUpdate.uid = YES;
|
|
}
|
|
|
|
j = [aScanner scanLocation];
|
|
}
|
|
//
|
|
// We read our flags. We usually get something like FLAGS (\Seen)
|
|
//
|
|
else if ([aWord caseInsensitiveCompare: @"FLAGS"] == NSOrderedSame) {
|
|
// We get the substring inside our ( )
|
|
aRange = [aMutableString rangeOfString: @")" options: 0 range: NSMakeRange(j,len-j)];
|
|
//LogInfo(@"Flags = |%@|", [aMutableString substringWithRange: NSMakeRange(j+2, aRange.location-j-2)]);
|
|
CWFlags *flagsBefore = aMessage.flags.copy;
|
|
[self _parseFlags: [aMutableString substringWithRange: NSMakeRange(j+2, aRange.location-j-2)]
|
|
message: aMessage
|
|
record: cacheRecord];
|
|
j = aRange.location + 1;
|
|
[aScanner setScanLocation: j];
|
|
if (!isMessageUpdate) {
|
|
messageUpdate.flags = YES;
|
|
} else if (aMessage.flags.rawFlagsAsShort != flagsBefore.rawFlagsAsShort) {
|
|
// For an existing message: trigger update only if the flags did actually change
|
|
messageUpdate.flags = YES;
|
|
}
|
|
}
|
|
//
|
|
// We read the RFC822 message size
|
|
//
|
|
else if ([aWord caseInsensitiveCompare: @"RFC822.SIZE"] == NSOrderedSame&& !isMessageUpdate) {
|
|
int size;
|
|
|
|
[aScanner scanInt: &size];
|
|
//LogInfo(@"size = %d", size);
|
|
[aMessage setSize: size];
|
|
cacheRecord.size = size;
|
|
|
|
j = [aScanner scanLocation];
|
|
|
|
messageUpdate.rfc822Size = YES;
|
|
}
|
|
//
|
|
// We must not break immediately after parsing this information. It's very important
|
|
// since servers like Exchange might send us responses like:
|
|
//
|
|
// * 1 FETCH (FLAGS (\Seen) RFC822.SIZE 4491 BODY[HEADER.FIELDS (From To Cc Subject Date Message-ID References In-Reply-To Content-Type)] {337} UID 614348)
|
|
//
|
|
// If we break right away, we'll skip the size and more importantly, the UID.
|
|
//
|
|
else if ([aWord caseInsensitiveCompare: @"BODY[HEADER]"] == NSOrderedSame && !isMessageUpdate) {
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] replaceCRLFWithLF];
|
|
[aMessage setHeadersFromData: [self.currentQueueObject.info objectForKey: @"NSData"] record: cacheRecord];
|
|
messageUpdate.bodyHeader = YES;
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if ([aWord caseInsensitiveCompare: @"BODY[TEXT]"] == NSOrderedSame && !isMessageUpdate) {
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] replaceCRLFWithLF];
|
|
if (![aMessage content]) {
|
|
NSData *aData;
|
|
|
|
//
|
|
// We do an initial check for the message body. If we haven't read a literal,
|
|
// [self.currentQueueObject.info objectForKey: @"NSData"] returns nil. This can
|
|
// happen with messages having a totally emtpy body. For those messages,
|
|
// we simply set a default content, being an empty NSData instance.
|
|
//
|
|
aData = [self.currentQueueObject.info objectForKey: @"NSData"];
|
|
|
|
if (!aData) aData = [NSData data];
|
|
|
|
[CWMIMEUtility setContentFromRawSource: aData inPart: aMessage];
|
|
[aMessage setInitialized: YES];
|
|
|
|
[self.currentQueueObject.info setObject: aMessage forKey: @"Message"];
|
|
|
|
messageUpdate.bodyText = YES;
|
|
[[_selectedFolder cacheManager] writeRecord: cacheRecord message: aMessage
|
|
messageUpdate: messageUpdate];
|
|
|
|
PERFORM_SELECTOR_2(_delegate, @selector(messagePrefetchCompleted:), PantomimeMessagePrefetchCompleted, aMessage, @"Message");
|
|
}
|
|
break;
|
|
}
|
|
//
|
|
//
|
|
//
|
|
else if (([aWord caseInsensitiveCompare: @"RFC822"] == NSOrderedSame ||
|
|
[aWord caseInsensitiveCompare: @"BODY[]"] == NSOrderedSame)
|
|
&& !isMessageUpdate) {
|
|
[[self.currentQueueObject.info objectForKey: @"NSData"] replaceCRLFWithLF];
|
|
|
|
NSData *aData = [self.currentQueueObject.info objectForKey: @"NSData"];
|
|
if (!aData) aData = [NSData data];
|
|
|
|
[aMessage setHeadersFromData: aData record: cacheRecord];
|
|
|
|
NSRange aRange = [aData rangeOfCString: "\n\n"];
|
|
if (aRange.location != NSNotFound) {
|
|
[CWMIMEUtility setContentFromRawSource:
|
|
[aData subdataWithRange: NSMakeRange(aRange.location + 2,
|
|
[aData length] - (aRange.location + 2))]
|
|
inPart: aMessage];
|
|
}
|
|
|
|
[aMessage setRawSource: aData];
|
|
|
|
[aMessage setInitialized: YES];
|
|
|
|
[self.currentQueueObject.info setObject: aMessage forKey: @"Message"];
|
|
|
|
messageUpdate.rfc822 = YES;
|
|
[[_selectedFolder cacheManager] writeRecord: cacheRecord message: aMessage
|
|
messageUpdate: messageUpdate];
|
|
|
|
PERFORM_SELECTOR_2(_delegate, @selector(messagePrefetchCompleted:),
|
|
PantomimeMessagePrefetchCompleted, aMessage, @"Message");
|
|
|
|
break;
|
|
}
|
|
|
|
i = j;
|
|
done = ![aScanner scanUpToCharactersFromSet: aCharacterSet intoString: NULL];
|
|
|
|
if (done && must_flush_record) {
|
|
if (isMessageUpdate && !messageUpdate.isNoChange) {
|
|
// If the message existed locally before (is update), bother the cache manager only
|
|
// if something has changed on server.
|
|
[[_selectedFolder cacheManager] writeRecord: cacheRecord message: aMessage
|
|
messageUpdate: messageUpdate];
|
|
}
|
|
}
|
|
}
|
|
|
|
RELEASE(aScanner);
|
|
RELEASE(aMutableString);
|
|
|
|
//
|
|
// It is important that we remove the responses we have processed. This is particularly
|
|
// useful if we are caching an IMAP mailbox. We could receive thousands of untagged
|
|
// FETCH responses and we don't want to go over them again and again everytime
|
|
// this method is invoked.
|
|
//
|
|
[_responsesFromServer removeObjectsInArray: aMutableArray];
|
|
RELEASE(aMutableArray);
|
|
RELEASE(cacheRecord);
|
|
}
|
|
|
|
|
|
//
|
|
// This command parses the result of a LIST command. See 7.2.2 for the complete
|
|
// description of the LIST response.
|
|
//
|
|
// Rationale:
|
|
//
|
|
// In IMAP, all mailboxes can hold messages and folders. Thus, the HOLDS_MESSAGES
|
|
// flag is ALWAYS set for a mailbox that has been parsed.
|
|
//
|
|
// We also support RFC3348 \HasChildren and \HasNoChildren flags. In fact, we
|
|
// directly map \HasChildren to HOLDS_FOLDERS.
|
|
//
|
|
// We support the following standard flags (from RFC3501):
|
|
//
|
|
// \Noinferiors
|
|
// It is not possible for any child levels of hierarchy to exist
|
|
// under this name; no child levels exist now and none can be
|
|
// created in the future.
|
|
//
|
|
// \Noselect
|
|
// It is not possible to use this name as a selectable mailbox.
|
|
//
|
|
// \Marked
|
|
// The mailbox has been marked "interesting" by the server; the
|
|
// mailbox probably contains messages that have been added since
|
|
// the last time the mailbox was selected.
|
|
//
|
|
// \Unmarked
|
|
// The mailbox does not contain any additional messages since the
|
|
// last time the mailbox was selected.
|
|
//
|
|
- (void) _parseLIST
|
|
{
|
|
NSString *aFolderName, *aString, *theString;
|
|
NSRange r1, r2;
|
|
NSUInteger len;
|
|
|
|
theString = [[_responsesFromServer lastObject] imapUtf7String];
|
|
//
|
|
// We verify if we got the number of bytes to read instead of the real mailbox name.
|
|
// That happens if we couldn't get the ASCII string of what we read.
|
|
//
|
|
// Some servers seem to send that when the mailbox name is 8-bit. Those 8-bit mailbox
|
|
// names were undefined in earlier versions of the IMAP protocol (now deprecated).
|
|
// See section 5.1. (Mailbox Naming) of RFC3051.
|
|
//
|
|
// The RFC says we SHOULD interpret that as UTF-8.
|
|
//
|
|
// If we got a 8-bit string, we rollback to get the previous answer in order
|
|
// to also decode the mailbox attribute.
|
|
//
|
|
if (!theString)
|
|
{
|
|
aFolderName = AUTORELEASE([[NSString alloc] initWithData: [_responsesFromServer lastObject] encoding: NSUTF8StringEncoding]);
|
|
|
|
// We get the "previous" line which contains our mailbox attributes
|
|
theString = [[_responsesFromServer objectAtIndex: [_responsesFromServer count]-2] asciiString];
|
|
}
|
|
else
|
|
{
|
|
// We get the folder name and the mailbox name attributes
|
|
aFolderName = [self _folderNameFromString: theString];
|
|
}
|
|
|
|
//
|
|
// If the folder name starts/ends with {}, that means it was "wrongly" encoded using
|
|
// 8-bit characters which are not allowed. We just return since we'll re-enter in
|
|
// _parseLIST whenever the real mailbox name will be read.
|
|
//
|
|
len = [aFolderName length];
|
|
if (len > 0 && [aFolderName characterAtIndex: 0] == '{' && [aFolderName characterAtIndex: len-1] == '}')
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We try to get our name attributes.
|
|
r1 = [theString rangeOfString: @"("];
|
|
|
|
if (r1.location == NSNotFound)
|
|
{
|
|
return;
|
|
}
|
|
|
|
r2 = [theString rangeOfString: @")" options: 0 range: NSMakeRange(r1.location+1, [theString length]-r1.location-1)];
|
|
|
|
if (r2.location == NSNotFound)
|
|
{
|
|
return;
|
|
}
|
|
|
|
aString = [theString substringWithRange: NSMakeRange(r1.location+1, r2.location-r1.location-1)];
|
|
|
|
// We get all the supported flags
|
|
PantomimeFolderAttribute folderAttributes = [self _folderAttributesForServerResponse:aString];
|
|
|
|
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:
|
|
@{PantomimeFolderNameKey: aFolderName,
|
|
PantomimeFolderFlagsKey: [NSNumber numberWithInteger: folderAttributes],
|
|
PantomimeFolderSeparatorKey: [NSString stringWithFormat:@"%c",
|
|
[self folderSeparator]]}
|
|
];
|
|
/* Get Special-Use attributes
|
|
According to RFC 6154 servers supporting spacial-use mailboxes send the CREATE-SPECIAL-USE capatibility.
|
|
We alway check for those attributes even the server does not mention "CREATE-SPECIAL-USE"
|
|
in the capabilities, as not all server promote this (for instance Yahoo!).
|
|
*/
|
|
PantomimeSpecialUseMailboxType specialUse = [self _specialUseTypeForServerResponse:aString];
|
|
if (specialUse != PantomimeSpecialUseMailboxNormal)
|
|
{
|
|
// A special-use mailbox purpose has been reported by the server.
|
|
userInfo[PantomimeFolderSpecialUseKey] = [NSNumber numberWithInteger: specialUse];
|
|
}
|
|
|
|
// Inform client about potential new folder, so it can be saved.
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderNameParsed:),
|
|
PantomimeFolderNameParsed, userInfo, PantomimeFolderInfo);
|
|
|
|
[_folders setObject: [NSNumber numberWithInteger: folderAttributes] forKey: aFolderName];
|
|
}
|
|
|
|
/**
|
|
Parses mailbox/folder attributes from a \LIST response.
|
|
@param listResponse server response for \LIST command for one folder
|
|
@return folder types
|
|
*/
|
|
- (PantomimeFolderAttribute)_folderAttributesForServerResponse:(NSString *)listResponse
|
|
{
|
|
PantomimeFolderAttribute type = PantomimeHoldsMessages;
|
|
|
|
// We get all the supported flags, starting with the flags of RFC3348
|
|
if ([listResponse length])
|
|
{
|
|
if ([listResponse rangeOfString: @"\\HasChildren" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
type |= PantomimeHoldsFolders;
|
|
}
|
|
|
|
if ([listResponse rangeOfString: @"\\NoInferiors" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
type |= PantomimeNoInferiors;
|
|
}
|
|
|
|
if ([listResponse rangeOfString: @"\\NoSelect" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
type |= PantomimeNoSelect;
|
|
}
|
|
|
|
if ([listResponse rangeOfString: @"\\Marked" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
type |= PantomimeMarked;
|
|
}
|
|
|
|
if ([listResponse rangeOfString: @"\\Unmarked" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
type |= PantomimeUnmarked;
|
|
}
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
/**
|
|
Parses Special-Use attributes for one mailbox/folder from a \LIST response. RFC 6154.
|
|
|
|
@param listResponse server response for \LIST command for one folder
|
|
@return special-use attribute
|
|
*/
|
|
- (PantomimeSpecialUseMailboxType)_specialUseTypeForServerResponse:(NSString *)listResponse
|
|
{
|
|
PantomimeSpecialUseMailboxType specialUse = PantomimeSpecialUseMailboxNormal;
|
|
// We get all the special-use attributes (RFC3348)
|
|
specialUse = PantomimeSpecialUseMailboxNormal;
|
|
|
|
if ([listResponse length])
|
|
{
|
|
if ([listResponse rangeOfString: @"\\All" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxAll;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Archive" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxArchive;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Drafts" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxDrafts;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Flagged" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxFlagged;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Junk" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxJunk;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Sent" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxSent;
|
|
}
|
|
if ([listResponse rangeOfString: @"\\Trash" options: NSCaseInsensitiveSearch].length)
|
|
{
|
|
specialUse = PantomimeSpecialUseMailboxTrash;
|
|
}
|
|
}
|
|
|
|
return specialUse;
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseLSUB
|
|
{
|
|
NSString *aString, *aFolderName;
|
|
NSUInteger len;
|
|
|
|
aString = [[NSString alloc] initWithData: [_responsesFromServer lastObject]
|
|
encoding: _defaultCStringEncoding];
|
|
|
|
if (!aString)
|
|
{
|
|
aFolderName = AUTORELEASE([[NSString alloc] initWithData: [_responsesFromServer lastObject]
|
|
encoding: NSUTF8StringEncoding]);
|
|
}
|
|
else
|
|
{
|
|
aFolderName = [self _folderNameFromString: aString];
|
|
RELEASE(aString);
|
|
}
|
|
|
|
|
|
// Check the rationale in _parseLIST.
|
|
len = [aFolderName length];
|
|
if (len > 0 && [aFolderName characterAtIndex: 0] == '{' && [aFolderName characterAtIndex: len-1] == '}')
|
|
{
|
|
RELEASE(aString);
|
|
return;
|
|
}
|
|
|
|
[_subscribedFolders addObject: aFolderName];
|
|
RELEASE(aString);
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseNO
|
|
{
|
|
// Synchronize all methods that alter the _queue
|
|
@synchronized(self) {
|
|
NSData *aData;
|
|
|
|
aData = [_responsesFromServer lastObject];
|
|
|
|
LogInfo(@"IN _parseNO: |%@| %d", [aData asciiString], _lastCommand);
|
|
|
|
switch (_lastCommand)
|
|
{
|
|
case IMAP_APPEND:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(folderAppendFailed:), PantomimeFolderAppendFailed, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_AUTHENTICATE_CRAM_MD5:
|
|
case IMAP_AUTHENTICATE_LOGIN:
|
|
case IMAP_AUTHENTICATE_XOAUTH2: // Only added for completenes. In reality XOAuth2 never responds with NO (only Gmail tested so far)
|
|
case IMAP_LOGIN:
|
|
AUTHENTICATION_FAILED(_delegate, _mechanism);
|
|
break;
|
|
|
|
case IMAP_CREATE:
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderCreateFailed:), PantomimeFolderCreateFailed);
|
|
break;
|
|
|
|
case IMAP_DELETE:
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderDeleteFailed:), PantomimeFolderDeleteFailed);
|
|
break;
|
|
|
|
case IMAP_EXPUNGE:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderExpungeFailed:), PantomimeFolderExpungeFailed, _selectedFolder, @"Folder");
|
|
break;
|
|
|
|
case IMAP_RENAME:
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderRenameFailed:), PantomimeFolderRenameFailed);
|
|
break;
|
|
|
|
case IMAP_SELECT:
|
|
_connection_state.opening_mailbox = NO;
|
|
if (!_selectedFolder) {
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderOpenFailed:), PantomimeFolderOpenFailed);
|
|
return;
|
|
}
|
|
if ([_selectedFolder.name isEqualToString:PantomimeFolderNameToIgnore]) {
|
|
// PantomimeFolderNameToIgnore is used as a workaround to close a mailbox (aka. folder)
|
|
// without calling CLOSE.
|
|
// see RFC4549-4.2.5
|
|
_selectedFolder = nil;
|
|
PERFORM_SELECTOR_2([self delegate], @selector(folderCloseCompleted:), PantomimeFolderCloseCompleted, self, @"Folder");
|
|
return;
|
|
}
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderOpenFailed:), PantomimeFolderOpenFailed, _selectedFolder, @"Folder");
|
|
[_openFolders removeObjectForKey: [_selectedFolder name]];
|
|
_selectedFolder = nil;
|
|
break;
|
|
|
|
case IMAP_SUBSCRIBE:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderSubscribeFailed:), PantomimeFolderSubscribeFailed, [self.currentQueueObject.info objectForKey: @"Name"], @"Name");
|
|
break;
|
|
|
|
case IMAP_UID_COPY:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messagesCopyFailed:), PantomimeMessagesCopyFailed, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_UID_MOVE:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messagesCopyFailed:), PantomimeMessageUidMoveFailed, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_UID_SEARCH_ALL:
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderSearchFailed:), PantomimeFolderSearchFailed);
|
|
break;
|
|
|
|
case IMAP_STATUS:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderStatusFailed:), PantomimeFolderStatusFailed, [self.currentQueueObject.info objectForKey: @"Name"], @"Name");
|
|
break;
|
|
|
|
case IMAP_UID_STORE:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messageStoreFailed:), PantomimeMessageStoreFailed, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_UNSUBSCRIBE:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderUnsubscribeFailed:), PantomimeFolderUnsubscribeFailed, [self.currentQueueObject.info objectForKey: @"Name"], @"Name");
|
|
break;
|
|
|
|
case IMAP_AUTHORIZATION:
|
|
case IMAP_CAPABILITY:
|
|
case IMAP_CLOSE:
|
|
case IMAP_EXAMINE:
|
|
case IMAP_LIST:
|
|
case IMAP_LOGOUT:
|
|
case IMAP_LSUB:
|
|
case IMAP_NOOP:
|
|
case IMAP_STARTTLS:
|
|
case IMAP_UID_FETCH_BODY_TEXT:
|
|
case IMAP_UID_FETCH_HEADER_FIELDS:
|
|
case IMAP_UID_FETCH_FLAGS:
|
|
case IMAP_UID_FETCH_HEADER_FIELDS_NOT:
|
|
case IMAP_UID_FETCH_RFC822:
|
|
case IMAP_UID_SEARCH:
|
|
case IMAP_UID_SEARCH_ANSWERED:
|
|
case IMAP_UID_SEARCH_FLAGGED:
|
|
case IMAP_UID_SEARCH_UNSEEN:
|
|
case IMAP_EMPTY_QUEUE: {
|
|
id nameValue = [[[self currentQueueObject] info] objectForKey:@"Name"];
|
|
if (nameValue) {
|
|
PERFORM_SELECTOR_2(_delegate, @selector(actionFailed:), PantomimeActionFailed,
|
|
nameValue, @"Name");
|
|
} else {
|
|
// Not all of the above commands have a name set, so fall back.
|
|
PERFORM_SELECTOR_1(_delegate, @selector(actionFailed:), PantomimeActionFailed);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
LogInfo(@"Unhandled \"NO\" response!");
|
|
NSAssert(false, @"");
|
|
break;
|
|
}
|
|
|
|
//
|
|
// If the NO response is tagged response, we remove the current
|
|
// queued object from the queue since it reached completion.
|
|
//
|
|
if (![aData hasCPrefix: "*"])//|| _lastCommand == IMAP_AUTHORIZATION)
|
|
{
|
|
//LogInfo(@"REMOVING QUEUE OBJECT");
|
|
|
|
[self.currentQueueObject.info setObject: [NSNumber numberWithInt: _lastCommand] forKey: @"Command"];
|
|
PERFORM_SELECTOR_3(_delegate, @selector(commandCompleted:), @"PantomimeCommandCompleted", self.currentQueueObject.info);
|
|
|
|
[_queue removeLastObject];
|
|
[self sendCommand: IMAP_EMPTY_QUEUE info: nil arguments: @""];
|
|
}
|
|
|
|
[_responsesFromServer removeAllObjects];
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// After sending a NOOP to the IMAP server, we might read untagged
|
|
// responses like * 5 RECENT that will eventually be processed.
|
|
//
|
|
- (void) _parseNOOP
|
|
{
|
|
//LogInfo(@"Parsing noop responses...");
|
|
//!
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseOK
|
|
{
|
|
// Synchronize all methods that alter the _queue
|
|
@synchronized(self) {
|
|
NSData *aData;
|
|
aData = [_responsesFromServer lastObject];
|
|
|
|
//LogInfo(@"IN _parseOK: |%@|", [aData asciiString]);
|
|
|
|
switch (_lastCommand)
|
|
{
|
|
case IMAP_APPEND:
|
|
//
|
|
// No need to do add the newly append messages to our internal messages holder as
|
|
// we will get an untagged * EXISTS response that will trigger the new FETCH
|
|
// RFC3501 says:
|
|
//
|
|
// If the mailbox is currently selected, the normal new message
|
|
// actions SHOULD occur. Specifically, the server SHOULD notify the
|
|
// client immediately via an untagged EXISTS response. If the server
|
|
// does not do so, the client MAY issue a NOOP command (or failing
|
|
// that, a CHECK command) after one or more APPEND commands.
|
|
//
|
|
PERFORM_SELECTOR_3(_delegate, @selector(folderAppendCompleted:), PantomimeFolderAppendCompleted, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_AUTHENTICATE_CRAM_MD5:
|
|
case IMAP_AUTHENTICATE_LOGIN:
|
|
case IMAP_AUTHENTICATE_XOAUTH2:
|
|
case IMAP_LOGIN:
|
|
if (_connection_state.reconnecting)
|
|
{
|
|
if (_selectedFolder)
|
|
{
|
|
if ([_selectedFolder mode] == PantomimeReadOnlyMode)
|
|
{
|
|
[self sendCommand: IMAP_EXAMINE info: nil arguments: @"EXAMINE \"%@\"", [[_selectedFolder name] modifiedUTF7String]];
|
|
}
|
|
else
|
|
{
|
|
[self sendCommand: IMAP_SELECT info: nil arguments: @"SELECT \"%@\"", [[_selectedFolder name] modifiedUTF7String]];
|
|
}
|
|
|
|
if (_connection_state.opening_mailbox) [_selectedFolder fetch];
|
|
}
|
|
else
|
|
{
|
|
[self _restoreQueue];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AUTHENTICATION_COMPLETED(_delegate, _mechanism);
|
|
}
|
|
break;
|
|
|
|
case IMAP_AUTHORIZATION:
|
|
if ([aData hasCPrefix: "* OK"])
|
|
{
|
|
[self sendCommand: IMAP_CAPABILITY info: nil arguments: @"CAPABILITY"];
|
|
}
|
|
else
|
|
{
|
|
//!
|
|
// connectionLost? or should we call [self close]?
|
|
}
|
|
break;
|
|
|
|
case IMAP_CLOSE:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(folderCloseCompleted:), PantomimeFolderCloseCompleted, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_CREATE:
|
|
[_folders setObject: [NSNumber numberWithInt: 0] forKey: [self.currentQueueObject.info objectForKey: @"Name"]];
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderCreateCompleted:), PantomimeFolderCreateCompleted);
|
|
break;
|
|
|
|
case IMAP_DELETE:
|
|
[_folders removeObjectForKey: [self.currentQueueObject.info objectForKey: @"Name"]];
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderDeleteCompleted:), PantomimeFolderDeleteCompleted);
|
|
break;
|
|
|
|
case IMAP_EXPUNGE:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderExpungeCompleted:), PantomimeFolderExpungeCompleted, _selectedFolder, @"Folder");
|
|
break;
|
|
|
|
case IMAP_LIST:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderListCompleted:), PantomimeFolderListCompleted, [_folders keyEnumerator], @"NSEnumerator");
|
|
break;
|
|
|
|
case IMAP_LOGOUT:
|
|
//! What should we do here?
|
|
[super close];
|
|
break;
|
|
|
|
case IMAP_LSUB:
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderListSubscribedCompleted:), PantomimeFolderListSubscribedCompleted, [_subscribedFolders objectEnumerator], @"NSEnumerator");
|
|
break;
|
|
|
|
case IMAP_RENAME:
|
|
[self _renameFolder];
|
|
PERFORM_SELECTOR_1(_delegate, @selector(folderRenameCompleted:), PantomimeFolderRenameCompleted);
|
|
break;
|
|
|
|
case IMAP_SELECT:
|
|
[self _parseSELECT];
|
|
break;
|
|
|
|
case IMAP_STARTTLS:
|
|
[self _parseSTARTTLS];
|
|
break;
|
|
|
|
case IMAP_SUBSCRIBE:
|
|
// We must add the folder to our list of subscribed folders.
|
|
[_subscribedFolders addObject: [self.currentQueueObject.info objectForKey: @"Name"]];
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderSubscribeCompleted:), PantomimeFolderSubscribeCompleted, [self.currentQueueObject.info objectForKey: @"Name"], @"Name");
|
|
break;
|
|
|
|
case IMAP_UID_COPY:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messagesCopyCompleted:), PantomimeMessagesCopyCompleted, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_UID_MOVE:
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messageUidMoveCompleted:), PantomimeMessageUidMoveCompleted, self.currentQueueObject.info);
|
|
break;
|
|
|
|
case IMAP_UID_FETCH_RFC822:
|
|
// fetchOlder() fetches the message for the oldest local UID to update its
|
|
// MSN before it actually fetches older messages.
|
|
// Thus it has to be called again after sucessfully updating the MSN.
|
|
if ([_selectedFolder fetchOlderNeedsReCall]) {
|
|
[_selectedFolder fetchOlderProtected];
|
|
break;
|
|
}
|
|
// Since we download mail all in one, we signal the
|
|
// end of fetch when all new mails have been downloadad.
|
|
_connection_state.opening_mailbox = NO;
|
|
if ([_selectedFolder cacheManager])
|
|
{
|
|
[[_selectedFolder cacheManager] synchronize];
|
|
}
|
|
//LogInfo(@"DONE FETCHING FOLDER");
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderFetchCompleted:), PantomimeFolderFetchCompleted, _selectedFolder, @"Folder");
|
|
break;
|
|
|
|
case IMAP_UID_FETCH_FLAGS: {
|
|
_connection_state.opening_mailbox = NO;
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderSyncCompleted:), PantomimeFolderSyncCompleted, _selectedFolder, @"Folder");
|
|
break;
|
|
}
|
|
|
|
case IMAP_UID_FETCH_UIDS: {
|
|
_connection_state.opening_mailbox = NO;
|
|
NSMutableDictionary *info = [NSMutableDictionary new];
|
|
if (_selectedFolder) {
|
|
info[@"Folder"] = _selectedFolder;
|
|
}
|
|
if (self.currentQueueObject.info[@"Uids"]) {
|
|
info[@"Uids"] = self.currentQueueObject.info[@"Uids"];
|
|
}
|
|
PERFORM_SELECTOR_3(_delegate, @selector(folderFetchCompleted:), PantomimeFolderFetchCompleted, info);
|
|
break;
|
|
}
|
|
|
|
case IMAP_UID_SEARCH_ALL:
|
|
//
|
|
// Before assuming we got a result and initialized everything in _parseSEARCH,
|
|
// we do a basic check. This is to prevent a rather weird behavior from
|
|
// UW IMAP Server, like this:
|
|
//
|
|
// . UID SEARCH ALL FROM "collaboration-world"
|
|
// * OK [PARSE] Unexpected characters at end of address: <>, Aix.p4@itii-paca.net...
|
|
// * SEARCH
|
|
// 000d OK UID SEARCH completed^
|
|
//
|
|
if ([self.currentQueueObject.info objectForKey: @"Results"])
|
|
{
|
|
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: _selectedFolder, @"Folder", [self.currentQueueObject.info objectForKey: @"Results"], @"Results", nil];
|
|
PERFORM_SELECTOR_3(_delegate, @selector(folderSearchCompleted:), PantomimeFolderSearchCompleted, userInfo);
|
|
}
|
|
break;
|
|
|
|
case IMAP_UID_STORE:
|
|
{
|
|
// Once STORE has completed, we update the messages.
|
|
NSArray *theMessages;
|
|
CWFlags *theFlags;
|
|
NSUInteger i, count;
|
|
|
|
theMessages = [self.currentQueueObject.info objectForKey: PantomimeMessagesKey];
|
|
theFlags = [self.currentQueueObject.info objectForKey: PantomimeFlagsKey];
|
|
count = [theMessages count];
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
[[(CWMessage *) [theMessages objectAtIndex: i] flags] replaceWithFlags: theFlags];
|
|
}
|
|
|
|
PERFORM_SELECTOR_3(_delegate, @selector(messageStoreCompleted:), PantomimeMessageStoreCompleted, self.currentQueueObject.info);
|
|
break;
|
|
}
|
|
|
|
case IMAP_UNSUBSCRIBE:
|
|
// We must remove the folder from our list of subscribed folders.
|
|
[_subscribedFolders removeObject: [self.currentQueueObject.info objectForKey: @"Name"]];
|
|
PERFORM_SELECTOR_2(_delegate, @selector(folderUnsubscribeCompleted:), PantomimeFolderUnsubscribeCompleted, [self.currentQueueObject.info objectForKey: @"Name"], @"Name");
|
|
break;
|
|
|
|
case IMAP_IDLE_DONE:
|
|
PERFORM_SELECTOR_1(_delegate, @selector(idleFinished:), PantomimeIdleFinished);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
//
|
|
// If the OK response is tagged response, we remove the current
|
|
// queued object from the queue since it reached completion.
|
|
//
|
|
if (![aData hasCPrefix: "*"])// || _lastCommand == IMAP_AUTHORIZATION)
|
|
{
|
|
//LogInfo(@"REMOVING QUEUE OBJECT");
|
|
if (self.currentQueueObject && self.currentQueueObject.info) {
|
|
[self.currentQueueObject.info
|
|
setObject: [NSNumber numberWithInt: _lastCommand] forKey: @"Command"];
|
|
PERFORM_SELECTOR_3(_delegate, @selector(commandCompleted:), @"PantomimeCommandCompleted", self.currentQueueObject.info);
|
|
} else {
|
|
LogInfo(@"self.currentQueueObject == nil");
|
|
}
|
|
|
|
[_queue removeLastObject];
|
|
[self sendCommand: IMAP_EMPTY_QUEUE info: nil arguments: @""];
|
|
}
|
|
|
|
[_responsesFromServer removeAllObjects];
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// This method receives a * 5 RECENT parameter and parses it.
|
|
//
|
|
- (void) _parseRECENT
|
|
{
|
|
// Do nothing for now. This breaks 7.3.2 since the response
|
|
// is not recorded.
|
|
}
|
|
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _parseSEARCH
|
|
{
|
|
NSMutableArray *aMutableArray;
|
|
CWIMAPMessage *aMessage;
|
|
NSArray *allResults;
|
|
NSUInteger i, count;
|
|
|
|
allResults = [self _uniqueIdentifiersFromSearchResponseData:[_responsesFromServer lastObject]];
|
|
count = [allResults count];
|
|
|
|
aMutableArray = [NSMutableArray array];
|
|
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
aMessage = [[_selectedFolder cacheManager] messageWithUID:
|
|
[[allResults objectAtIndex: i] unsignedIntValue]];
|
|
|
|
if (aMessage)
|
|
{
|
|
[aMutableArray addObject: aMessage];
|
|
}
|
|
else
|
|
{
|
|
//LogInfo(@"Message with UID = %u not found in cache.",
|
|
|