1 // Copyright 2009 The Archiveopteryx Developers <info@aox.org>
5 #include "imapsession.h"
6 #include "imapparser.h"
7 #include "annotation.h"
8 #include "integerset.h"
21 static const char * legalAnnotationAttributes[] = {
34 : uid( false ), done( false ), codec( 0 ), root( 0 ),
35 query( 0 ), highestmodseq( 1 ),
36 firstmodseq( 1 ), lastmodseq( 1 ),
37 returnModseq( false ),
38 returnAll( false ), returnCount( false ),
39 returnMax( false ), returnMin( false )
64 /*! \class Search search.h
65 Finds messages matching some criteria (RFC 3501 section 6.4.4)
67 The entirety of the basic syntax is handled, as well as ESEARCH
68 (RFC 4731 and RFC 4466), of CONDSTORE (RFC 4551), ANNOTATE (RFC
69 5257) and WITHIN (RFC 5032).
71 Searches are first run against the RAM cache, rudimentarily. If
72 the comparison is difficult, expensive or unsuccessful, it gives
73 up and uses the database.
75 If ESEARCH with only MIN, only MAX or only COUNT is used, we could
76 generate better SQL than we do. Let's do that optimisation when a
77 client benefits from it.
81 /*! Constructs an empty Search. If \a u is true, it's a UID SEARCH,
82 otherwise it's the MSN variety.
85 Search::Search( bool u )
94 d->root = new Selector;
101 if ( present( "return" ) ) {
102 // RFC 4731 and RFC 4466 define ESEARCH together.
106 while ( ok() && nextChar() != ')' &&
107 nextChar() >= 'A' && nextChar() <= 'z' ) {
108 EString modifier = letters( 3, 5 ).lower();
110 if ( modifier == "all" )
112 else if ( modifier == "min" )
114 else if ( modifier == "max" )
116 else if ( modifier == "count" )
117 d->returnCount = true;
119 error( Bad, "Unknown search modifier option: " + modifier );
120 if ( nextChar() != ')' )
128 if ( present ( "charset" ) ) {
130 setCharset( astring() );
133 d->root = new Selector;
134 d->root->add( parseKey() );
135 while ( ok() && !parser()->atEnd() ) {
137 d->root->add( parseKey() );
141 d->returnModseq = d->root->usesModseq();
143 log( "Search for " + d->root->debugString() );
147 /*! Parse one search key (IMAP search-key) and returns a pointer to
148 the corresponding Selector. Leaves the cursor on the first
149 character following the search-key.
152 Selector * Search::parseKey()
157 // it's an "and" list.
158 Selector * s = new Selector( Selector::And );
159 s->add( parseKey() );
160 while ( ok() && !present( ")" ) ) {
162 s->add( parseKey() );
166 else if ( c == '*' || ( c >= '0' && c <= '9' ) ) {
168 return new Selector( set( true ) );
170 else if ( present( "older" ) ) {
172 return new Selector( Selector::Age, Selector::Larger, nzNumber() );
174 else if ( present( "younger" ) ) {
176 return new Selector( Selector::Age, Selector::Smaller, nzNumber() );
178 else if ( present( "all" ) ) {
179 return new Selector( Selector::NoField, Selector::All );
181 else if ( present( "answered" ) ) {
182 return new Selector( Selector::Flags, Selector::Contains,
185 else if ( present( "deleted" ) ) {
186 return new Selector( Selector::Flags, Selector::Contains,
189 else if ( present( "flagged" ) ) {
190 return new Selector( Selector::Flags, Selector::Contains,
193 else if ( present( "new" ) ) {
194 Selector * s = new Selector( Selector::And );
195 s->add( new Selector( Selector::Flags, Selector::Contains,
197 Selector * n = new Selector( Selector::Not );
199 n->add( new Selector( Selector::Flags, Selector::Contains,
203 else if ( present( "old" ) ) {
204 Selector * s = new Selector( Selector::Not );
205 s->add( new Selector( Selector::Flags, Selector::Contains,
209 else if ( present( "recent" ) ) {
210 return new Selector( Selector::Flags, Selector::Contains,
213 else if ( present( "seen" ) ) {
214 return new Selector( Selector::Flags, Selector::Contains,
217 else if ( present( "unanswered" ) ) {
218 Selector * s = new Selector( Selector::Not );
219 s->add(new Selector( Selector::Flags, Selector::Contains,
223 else if ( present( "undeleted" ) ) {
224 Selector * s = new Selector( Selector::Not );
225 s->add( new Selector( Selector::Flags, Selector::Contains,
229 else if ( present( "unflagged" ) ) {
230 Selector * s = new Selector( Selector::Not );
231 s->add( new Selector( Selector::Flags, Selector::Contains,
235 else if ( present( "unseen" ) ) {
236 Selector * s = new Selector( Selector::Not );
237 s->add( new Selector( Selector::Flags, Selector::Contains,
241 else if ( present( "draft" ) ) {
242 return new Selector( Selector::Flags, Selector::Contains,
245 else if ( present( "undraft" ) ) {
246 Selector * s = new Selector( Selector::Not );
247 s->add( new Selector( Selector::Flags, Selector::Contains,
251 else if ( present( "on" ) ) {
253 return new Selector( Selector::InternalDate, Selector::OnDate,
256 else if ( present( "before" ) ) {
258 return new Selector( Selector::InternalDate, Selector::BeforeDate,
261 else if ( present( "since" ) ) {
263 return new Selector( Selector::InternalDate, Selector::SinceDate,
266 else if ( present( "sentbefore" ) ) {
268 return new Selector( Selector::Sent, Selector::BeforeDate, date() );
270 else if ( present( "senton" ) ) {
272 return new Selector( Selector::Sent, Selector::OnDate, date() );
274 else if ( present( "sentsince" ) ) {
276 return new Selector( Selector::Sent, Selector::SinceDate, date() );
278 else if ( present( "from" ) ) {
280 return new Selector( Selector::Header, Selector::Contains,
281 "from", ustring( AString ) );
283 else if ( present( "to" ) ) {
285 return new Selector( Selector::Header, Selector::Contains,
286 "to", ustring( AString ) );
288 else if ( present( "cc" ) ) {
290 return new Selector( Selector::Header, Selector::Contains,
291 "cc", ustring( AString ) );
293 else if ( present( "bcc" ) ) {
295 return new Selector( Selector::Header, Selector::Contains,
296 "bcc", ustring( AString ) );
298 else if ( present( "subject" ) ) {
300 return new Selector( Selector::Header, Selector::Contains,
301 "subject", ustring( AString ) );
303 else if ( present( "body" ) ) {
305 return new Selector( Selector::Body, Selector::Contains,
306 ustring( AString ) );
308 else if ( present( "text" ) ) {
310 UString a = ustring( AString );
311 Selector * o = new Selector( Selector::Or );
312 o->add( new Selector( Selector::Body, Selector::Contains, a ) );
313 // field name is null for any-field searches
314 o->add( new Selector( Selector::Header, Selector::Contains, 0, a ) );
317 else if ( present( "keyword" ) ) {
319 return new Selector( Selector::Flags, Selector::Contains,
322 else if ( present( "unkeyword" ) ) {
324 Selector * s = new Selector( Selector::Not );
325 s->add( new Selector( Selector::Flags, Selector::Contains, atom() ) );
328 else if ( present( "header" ) ) {
330 EString s1 = astring();
332 UString s2 = ustring( AString );
333 return new Selector( Selector::Header, Selector::Contains, s1, s2 );
335 else if ( present( "uid" ) ) {
337 return new Selector( set( false ) );
339 else if ( present( "or" ) ) {
341 Selector * s = new Selector( Selector::Or );
342 s->add( parseKey() );
344 s->add( parseKey() );
347 else if ( present( "not" ) ) {
349 Selector * s = new Selector( Selector::Not );
350 s->add( parseKey() );
353 else if ( present( "larger" ) ) {
355 return new Selector( Selector::Rfc822Size, Selector::Larger,
358 else if ( present( "smaller" ) ) {
360 return new Selector( Selector::Rfc822Size, Selector::Smaller,
363 else if ( present( "msgid" ) ) {
365 return new Selector( Selector::DatabaseId, Selector::Equals,
368 else if ( present( "thrid" ) ) {
370 return new Selector( Selector::ThreadId, Selector::Equals,
373 else if ( present( "annotation" ) ) {
375 EString a = parser()->listMailbox();
376 if ( !parser()->ok() )
377 error( Bad, parser()->error() );
381 UString c = ustring( NString );
384 while ( ::legalAnnotationAttributes[i] &&
385 b != ::legalAnnotationAttributes[i] )
387 if ( !::legalAnnotationAttributes[i] )
388 error( Bad, "Unknown annotation attribute: " + b );
390 return new Selector( Selector::Annotation, Selector::Contains,
393 else if ( present( "modseq" ) ) {
395 if ( nextChar() == '"' ) {
396 // we don't store per-flag or per-annotation modseqs,
397 // so RFC 4551 3.4 says we MUST ignore these
398 (void)quoted(); // flag or annotation name
400 (void)letters( 3, 6 ); // priv/shared/all
403 return new Selector( Selector::Modseq, Selector::Larger,
406 else if ( present( "inthread" ) ) {
408 Selector * s = new Selector( Selector::InThread );
409 s->add( parseKey() );
413 error( Bad, "expected search key, saw: " + following() );
418 void Search::execute()
420 if ( state() != Executing )
424 ( d->query->state() == Query::Submitted ||
425 d->query->state() == Query::Executing ) ) {
426 if ( imap()->Connection::state() != Connection::Connected ) {
427 Database::cancelQuery( d->query );
428 error( No, "Client disconnected" );
431 else if ( imap()->state() == IMAP::Logout ) {
432 Database::cancelQuery( d->query );
433 error( No, "Client logged out" );
438 ImapSession * s = session();
448 d->query = d->root->query( imap()->user(), s->mailbox(),
453 if ( !d->query->done() )
456 if ( d->query->failed() ) {
457 error( No, "Database error: " + d->query->error() );
461 bool firstRow = true;
463 while ( (r=d->query->nextRow()) != 0 ) {
464 d->matches.add( r->getInt( "uid" ) );
465 if ( d->returnModseq ) {
466 int64 ms = r->getBigint( "modseq" );
471 if ( ms > d->highestmodseq )
472 d->highestmodseq = ms;
481 /*! Considers whether this search can and should be solved using this
482 cache, and if so, finds all the matches.
485 void Search::considerCache()
487 if ( d->returnModseq )
489 Session * s = imap()->session();
494 else if ( d->root->field() == Selector::Uid &&
495 d->root->action() == Selector::Contains ) {
496 d->matches = s->messages().intersection( d->root->messageSet() );
497 log( "UID-only search matched " +
498 fn( d->matches.count() ) + " messages",
502 uint max = s->count();
503 // don't consider more than 300 messages - pg does it better
507 while ( c < max && !needDb ) {
509 uint uid = s->uid( c );
510 switch ( d->root->match( s, uid ) ) {
512 d->matches.add( uid );
517 log( "Search must go to database: message " + fn( uid ) +
518 " could not be tested in RAM",
525 log( "Search considered " + fn( c ) + " of " + fn( max ) +
526 " messages using cache", Log::Debug );
534 /*! Parses the IMAP date production and returns the string (sans
535 quotes). Month names are case-insensitive; RFC 3501 is not
536 entirely clear about that. */
538 EString Search::date()
540 // date-day "-" date-month "-" date-year
549 result.append( digits( 1, 2 ) );
550 if ( nextChar() != '-' )
551 error( Bad, "expected -, saw " + following() );
552 uint day = result.number( 0 );
553 if ( result.length() < 2 )
554 result = "0" + result;
555 result.append( "-" );
557 EString month = letters( 3, 3 ).lower();
558 if ( month == "jan" || month == "feb" || month == "mar" ||
559 month == "apr" || month == "may" || month == "jun" ||
560 month == "jul" || month == "aug" || month == "sep" ||
561 month == "oct" || month == "nov" || month == "dec" )
562 result.append( month );
564 error( Bad, "Expected three-letter month name, received " + month );
565 if ( nextChar() != '-' )
566 error( Bad, "expected -, saw " + following() );
567 result.append( "-" );
569 uint year = digits( 4, 4 ).number( 0 );
571 error( Bad, "Years before 1500 not supported" );
572 result.append( EString::fromNumber( year ) );
574 if ( nextChar() != '"' )
575 error( Bad, "Expected \", saw " + following() );
580 tmp.setDate( year, month, day, 0, 0, 0, 0 );
582 error( Bad, "Invalid date: " + result );
587 /*! Reads an argument of type \a stringType (which may be AString,
588 NString, or PlainString) and returns it as unicode, using the
589 charset specified in the CHARSET argument to SEARCH.
592 UString Search::ustring( Command::QuoteMode stringType )
596 else if ( imap()->clientSupports( IMAP::Unicode ) )
597 d->codec = new Utf8Codec;
598 else if ( !d->codec )
599 d->codec = new AsciiCodec;
614 UString canon = d->codec->toUnicode( raw );
615 if ( !d->codec->valid() )
617 "astring not valid under encoding " + d->codec->name() +
623 /*! This helper function is called by the parser to set the CHARSET for
627 void Search::setCharset( const EString &s )
630 d->codec = Codec::byName( d->charset );
634 EString r = "[BADCHARSET";
635 EStringList::Iterator i( Codec::allCodecNames() );
638 r.append( imapQuoted( *i, AString ) );
641 r.append( "] Unknown character encoding: " );
642 r.append( d->charset.simplified() );
648 /*! Returns the root Selector constructed while parsing this Search
652 Selector * Search::selector() const
658 /*! This reimplementation of Command::set() simplifies the set by
659 including messages that don't exist. \a parseMsns is as for
663 IntegerSet Search::set( bool parseMsns )
665 IntegerSet s( Command::set( parseMsns ) );
670 static uint max( uint a, uint b )
678 /*! Makes sure a SEARCH or ESEARCH response is sent, whichever is
682 void Search::sendResponse()
684 int64 ms = d->highestmodseq;
685 if ( !d->returnModseq )
686 ms = 0; // means to send none
687 else if ( d->returnAll || d->returnCount )
688 ms = d->highestmodseq;
689 else if ( d->returnMin && d->returnMax )
690 ms = max( d->firstmodseq, d->lastmodseq );
691 else if ( d->returnMin )
693 else if ( d->returnMax )
695 waitFor( new ImapSearchResponse( session(), d->matches, ms, tag(),
704 /*! \class ImapSearchResponse search.h
706 The ImapSearchResponse models the SEARCH and ESEARCH responses. It
707 is responsible for sending the right one, and for using only
713 /*! Constructs a search response, able to send a SEARCH or ESEARCH
714 response for \a set within \a session.
716 If \a u is true, UIDs will be sent, if not, MSNs. If a modseq
717 needs to be sent, \a modseq will be. If the response is ESEARCH,
718 then \a tag will be included as command tag.
720 The \a rmin, \a rmax, \a rcount and \a rall response modifiers
721 correspond to the four result options in RFC 4731.
724 ImapSearchResponse::ImapSearchResponse( ImapSession * session,
725 const IntegerSet & set, int64 modseq,
728 bool rmin, bool rmax,
729 bool rcount, bool rall )
730 : ImapResponse( session ), r( set ), ms( modseq ), t( tag ),
731 uid( u ), min( rmin ), max( rmax ), count( rcount ), all( rall )
736 static void appendUid( EString & r, Session * s, bool u, uint uid )
739 r.appendNumber( uid );
742 uint m = s->msn( uid );
749 /*! Constructs a SEARCH or ESEARCH response depending on. */
751 EString ImapSearchResponse::text() const
753 Session * s = session();
755 result.reserve( r.count() * 10 );
756 if ( all || max || min || count ) {
757 result.append( "ESEARCH (tag " );
758 result.append( t.quoted() );
759 result.append( ")" );
761 result.append( " uid" );
763 result.append( " count " );
764 result.appendNumber( r.count() );
770 result.append( " min " );
771 appendUid( result, s, uid, r.smallest() );
774 result.append( " max " );
775 appendUid( result, s, uid, r.largest() );
778 result.append( " all " );
780 result.append( r.set() );
785 uint max = r.count();
787 uint m = s->msn( r.value( i ) );
792 result.append( msns.set() );
796 result.append( " modseq " );
797 result.appendNumber( ms );
801 result.reserve( r.count() * 10 );
802 result.append( "SEARCH" );
804 uint max = r.count();
806 result.append( " " );
807 appendUid( result, s, uid, r.value( i ) );
811 result.append( " (modseq " );
812 result.appendNumber( ms );
813 result.append( ")" );