1/*
2 * Copyright 2011-2016, Haiku, Inc. All rights reserved.
3 * Copyright 2011, Clemens Zeidler <haiku@clemens-zeidler.de>
4 * Distributed under the terms of the MIT License.
5 */
6
7
8#include "Commands.h"
9
10#include <stdlib.h>
11
12#include <AutoDeleter.h>
13
14
15#define DEBUG_IMAP_HANDLER
16#ifdef DEBUG_IMAP_HANDLER
17#	include <stdio.h>
18#	define TRACE(x...) printf(x)
19#else
20#	define TRACE(x...) ;
21#endif
22
23
24using namespace BPrivate;
25
26
27/*!	Maximum size of commands to the server (soft limit) */
28const size_t kMaxCommandLength = 2048;
29
30
31static void
32PutFlag(BString& string, const char* flag)
33{
34	if (!string.IsEmpty())
35		string += " ";
36	string += flag;
37}
38
39
40static BString
41GenerateFlagString(uint32 flags)
42{
43	BString string;
44
45	if ((flags & IMAP::kSeen) != 0)
46		PutFlag(string, "\\Seen");
47	if ((flags & IMAP::kAnswered) != 0)
48		PutFlag(string, "\\Answered");
49	if ((flags & IMAP::kFlagged) != 0)
50		PutFlag(string, "\\Flagged");
51	if ((flags & IMAP::kDeleted) != 0)
52		PutFlag(string, "\\Deleted");
53	if ((flags & IMAP::kDraft) != 0)
54		PutFlag(string, "\\Draft");
55
56	return string;
57}
58
59
60static uint32
61ParseFlags(IMAP::ArgumentList& list)
62{
63	uint32 flags = 0;
64	for (int32 i = 0; i < list.CountItems(); i++) {
65		if (list.EqualsAt(i, "\\Seen"))
66			flags |= IMAP::kSeen;
67		else if (list.EqualsAt(i, "\\Answered"))
68			flags |= IMAP::kAnswered;
69		else if (list.EqualsAt(i, "\\Flagged"))
70			flags |= IMAP::kFlagged;
71		else if (list.EqualsAt(i, "\\Deleted"))
72			flags |= IMAP::kDeleted;
73		else if (list.EqualsAt(i, "\\Draft"))
74			flags |= IMAP::kDraft;
75	}
76	return flags;
77}
78
79
80// #pragma mark -
81
82
83namespace IMAP {
84
85
86Handler::Handler()
87{
88}
89
90
91Handler::~Handler()
92{
93}
94
95
96// #pragma mark -
97
98
99Command::~Command()
100{
101}
102
103
104status_t
105Command::HandleTagged(Response& response)
106{
107	if (response.StringAt(0) == "OK")
108		return B_OK;
109	if (response.StringAt(0) == "BAD")
110		return B_BAD_VALUE;
111	if (response.StringAt(0) == "NO")
112		return B_NOT_ALLOWED;
113
114	return B_ERROR;
115}
116
117
118// #pragma mark -
119
120#if 0
121HandlerListener::~HandlerListener()
122{
123}
124
125
126void
127HandlerListener::ExpungeReceived(int32 number)
128{
129}
130
131
132void
133HandlerListener::ExistsReceived(int32 number)
134{
135}
136#endif
137
138// #pragma mark -
139
140
141RawCommand::RawCommand(const BString& command)
142	:
143	fCommand(command)
144{
145}
146
147
148BString
149RawCommand::CommandString()
150{
151	return fCommand;
152}
153
154
155// #pragma mark -
156
157
158LoginCommand::LoginCommand(const char* user, const char* password)
159	:
160	fUser(user),
161	fPassword(password)
162{
163}
164
165
166BString
167LoginCommand::CommandString()
168{
169	BString command = "LOGIN ";
170	command << "\"" << fUser << "\" " << "\"" << fPassword << "\"";
171
172	return command;
173}
174
175
176bool
177LoginCommand::HandleUntagged(Response& response)
178{
179	if (!response.EqualsAt(0, "OK") || !response.IsListAt(1, '['))
180		return false;
181
182	// TODO: we only support capabilities at the moment
183	ArgumentList& list = response.ListAt(1);
184	if (!list.EqualsAt(0, "CAPABILITY"))
185		return false;
186
187	fCapabilities.MakeEmpty();
188	while (list.CountItems() > 1)
189		fCapabilities.AddItem(list.RemoveItemAt(1));
190
191	TRACE("CAPABILITY: %s\n", fCapabilities.ToString().String());
192	return true;
193}
194
195
196// #pragma mark -
197
198
199SelectCommand::SelectCommand()
200	:
201	fNextUID(0),
202	fUIDValidity(0)
203{
204}
205
206
207SelectCommand::SelectCommand(const char* name)
208	:
209	fNextUID(0),
210	fUIDValidity(0)
211{
212	SetTo(name);
213}
214
215
216BString
217SelectCommand::CommandString()
218{
219	if (fMailboxName == "")
220		return "";
221
222	BString command = "SELECT \"";
223	command += fMailboxName;
224	command += "\"";
225	return command;
226}
227
228
229bool
230SelectCommand::HandleUntagged(Response& response)
231{
232	if (response.EqualsAt(0, "OK") && response.IsListAt(1, '[')) {
233		const ArgumentList& arguments = response.ListAt(1);
234		if (arguments.EqualsAt(0, "UIDVALIDITY")
235			&& arguments.IsNumberAt(1)) {
236			fUIDValidity = arguments.NumberAt(1);
237			return true;
238		} else if (arguments.EqualsAt(0, "UIDNEXT")
239			&& arguments.IsNumberAt(1)) {
240			fNextUID = arguments.NumberAt(1);
241			return true;
242		}
243	}
244
245	return false;
246}
247
248
249void
250SelectCommand::SetTo(const char* mailboxName)
251{
252	RFC3501Encoding encoding;
253	fMailboxName = encoding.Encode(mailboxName);
254}
255
256
257// #pragma mark -
258
259
260BString
261CapabilityHandler::CommandString()
262{
263	return "CAPABILITY";
264}
265
266
267bool
268CapabilityHandler::HandleUntagged(Response& response)
269{
270	if (!response.IsCommand("CAPABILITY"))
271		return false;
272
273	fCapabilities.MakeEmpty();
274	while (response.CountItems() > 1)
275		fCapabilities.AddItem(response.RemoveItemAt(1));
276
277	TRACE("CAPABILITY: %s\n", fCapabilities.ToString().String());
278	return true;
279}
280
281
282// #pragma mark -
283
284
285FetchMessageEntriesCommand::FetchMessageEntriesCommand(
286	MessageEntryList& entries, uint32 from, uint32 to, bool uids)
287	:
288	fEntries(entries),
289	fFrom(from),
290	fTo(to),
291	fUIDs(uids)
292{
293}
294
295
296BString
297FetchMessageEntriesCommand::CommandString()
298{
299	BString command = fUIDs ? "UID FETCH " : "FETCH ";
300	command << fFrom;
301	if (fFrom != fTo)
302		command << ":" << fTo;
303
304	command << " (FLAGS RFC822.SIZE";
305	if (!fUIDs)
306		command << " UID";
307	command << ")";
308
309	return command;
310}
311
312
313bool
314FetchMessageEntriesCommand::HandleUntagged(Response& response)
315{
316	if (!response.EqualsAt(1, "FETCH") || !response.IsListAt(2))
317		return false;
318
319	MessageEntry entry;
320
321	ArgumentList& list = response.ListAt(2);
322	for (int32 i = 0; i < list.CountItems(); i += 2) {
323		if (list.EqualsAt(i, "UID") && list.IsNumberAt(i + 1))
324			entry.uid = list.NumberAt(i + 1);
325		else if (list.EqualsAt(i, "RFC822.SIZE") && list.IsNumberAt(i + 1))
326			entry.size = list.NumberAt(i + 1);
327		else if (list.EqualsAt(i, "FLAGS") && list.IsListAt(i + 1)) {
328			// Parse flags
329			ArgumentList& flags = list.ListAt(i + 1);
330			entry.flags = ParseFlags(flags);
331		}
332	}
333
334	if (entry.uid == 0)
335		return false;
336
337	fEntries.push_back(entry);
338	return true;
339}
340
341
342// #pragma mark -
343
344
345FetchCommand::FetchCommand(uint32 from, uint32 to, uint32 flags)
346	:
347	fFlags(flags)
348{
349	fSequence << from;
350	if (from != to)
351		fSequence << ":" << to;
352}
353
354
355/*!	Builds the sequence from the passed in UID list, and takes \a max entries
356	at maximum. If the sequence gets too large, it might fetch less entries
357	than specified. The fetched UIDs will be removed from the \uids list.
358*/
359FetchCommand::FetchCommand(MessageUIDList& uids, size_t max, uint32 flags)
360	:
361	fFlags(flags)
362{
363	// Build sequence string
364	max = std::min(max, uids.size());
365
366	size_t index = 0;
367	while (index < max && (size_t)fSequence.Length() < kMaxCommandLength) {
368		// Get start of range
369		uint32 first = uids[index++];
370		uint32 last = first;
371		if (!fSequence.IsEmpty())
372			fSequence += ",";
373		fSequence << first;
374
375		for (; index < max; index++) {
376			uint32 uid = uids[index];
377			if (uid != last + 1)
378				break;
379
380			last = uid;
381		}
382
383		if (last != first)
384			fSequence << ":" << last;
385	}
386
387	uids.erase(uids.begin(), uids.begin() + index);
388}
389
390
391void
392FetchCommand::SetListener(FetchListener* listener)
393{
394	fListener = listener;
395}
396
397
398BString
399FetchCommand::CommandString()
400{
401	BString command = "UID FETCH ";
402	command += fSequence;
403
404	command += " (UID ";
405	if ((fFlags & kFetchFlags) != 0)
406		command += "FLAGS ";
407	switch (fFlags & kFetchAll) {
408		case kFetchHeader:
409			command += "RFC822.HEADER";
410			break;
411		case kFetchBody:
412			command += "BODY.PEEK[TEXT]";
413			break;
414		case kFetchAll:
415			command += "BODY.PEEK[]";
416			break;
417	}
418	command += ")";
419
420	return command;
421}
422
423
424bool
425FetchCommand::HandleUntagged(Response& response)
426{
427	if (!response.EqualsAt(1, "FETCH") || !response.IsListAt(2))
428		return false;
429
430	ArgumentList& list = response.ListAt(2);
431	uint32 uid = 0;
432	uint32 flags = 0;
433
434	for (int32 i = 0; i < list.CountItems(); i += 2) {
435		if (list.EqualsAt(i, "UID") && list.IsNumberAt(i + 1))
436			uid = list.NumberAt(i + 1);
437		else if (list.EqualsAt(i, "FLAGS") && list.IsListAt(i + 1)) {
438			// Parse flags
439			ArgumentList& flagList = list.ListAt(i + 1);
440			flags = ParseFlags(flagList);
441		}
442	}
443
444	if (fListener != NULL)
445		fListener->FetchedData(fFlags, uid, flags);
446	return true;
447}
448
449
450bool
451FetchCommand::HandleLiteral(Response& response, ArgumentList& arguments,
452	BDataIO& stream, size_t& length)
453{
454	if (fListener == NULL || !response.EqualsAt(1, "FETCH")
455		|| !response.IsListAt(2))
456		return false;
457
458	return fListener->FetchData(fFlags, stream, length);
459}
460
461
462// #pragma mark -
463
464
465SetFlagsCommand::SetFlagsCommand(uint32 uid, uint32 flags)
466	:
467	fUID(uid),
468	fFlags(flags)
469{
470}
471
472
473BString
474SetFlagsCommand::CommandString()
475{
476	BString command = "UID STORE ";
477	command << fUID << " FLAGS (" << GenerateFlagString(fFlags) << ")";
478
479	return command;
480}
481
482
483bool
484SetFlagsCommand::HandleUntagged(Response& response)
485{
486	// We're not that interested in the outcome, apparently
487	return response.EqualsAt(1, "FETCH");
488}
489
490
491// #pragma mark -
492
493
494#if 0
495AppendCommand::AppendCommand(IMAPMailbox& mailbox, BPositionIO& message,
496	off_t size, int32 flags, time_t time)
497	:
498	IMAPMailboxCommand(mailbox),
499
500	fMessageData(message),
501	fDataSize(size),
502	fFlags(flags),
503	fTime(time)
504{
505}
506
507
508BString
509AppendCommand::CommandString()
510{
511	BString command = "APPEND ";
512	command << fIMAPMailbox.Mailbox();
513	command += " (";
514	command += SetFlagsCommand::GenerateFlagList(fFlags);
515	command += ")";
516	command += " {";
517	command << fDataSize;
518	command += "}";
519	return command;
520}
521
522
523bool
524AppendCommand::HandleUntagged(const BString& response)
525{
526	if (response.FindFirst("+") != 0)
527		return false;
528	fMessageData.Seek(0, SEEK_SET);
529
530	const int32 kBunchSize = 1024; // 1Kb
531	char buffer[kBunchSize];
532
533	int32 writeSize = fDataSize;
534	while (writeSize > 0) {
535		int32 bunchSize = writeSize < kBunchSize ? writeSize : kBunchSize;
536		fMessageData.Read(buffer, bunchSize);
537		int nWritten = fIMAPMailbox.SendRawData(buffer, bunchSize);
538		if (nWritten < 0)
539			return false;
540		writeSize -= nWritten;
541		TRACE("%i\n", (int)writeSize);
542	}
543
544	fIMAPMailbox.SendRawData(CRLF, strlen(CRLF));
545	return true;
546}
547#endif
548
549
550// #pragma mark -
551
552
553ExistsHandler::ExistsHandler()
554{
555}
556
557
558void
559ExistsHandler::SetListener(ExistsListener* listener)
560{
561	fListener = listener;
562}
563
564
565bool
566ExistsHandler::HandleUntagged(Response& response)
567{
568	if (!response.EqualsAt(1, "EXISTS") || !response.IsNumberAt(0))
569		return false;
570
571	uint32 count = response.NumberAt(0);
572
573	if (fListener != NULL)
574		fListener->MessageExistsReceived(count);
575
576	return true;
577}
578
579
580// #pragma mark -
581
582
583ExpungeCommand::ExpungeCommand()
584{
585}
586
587
588BString
589ExpungeCommand::CommandString()
590{
591	return "EXPUNGE";
592}
593
594
595// #pragma mark -
596
597
598ExpungeHandler::ExpungeHandler()
599{
600}
601
602
603void
604ExpungeHandler::SetListener(ExpungeListener* listener)
605{
606	fListener = listener;
607}
608
609
610bool
611ExpungeHandler::HandleUntagged(Response& response)
612{
613	if (!response.EqualsAt(1, "EXPUNGE") || !response.IsNumberAt(0))
614		return false;
615
616	uint32 index = response.NumberAt(0);
617
618	if (fListener != NULL)
619		fListener->MessageExpungeReceived(index);
620
621	return true;
622}
623
624
625// #pragma mark -
626
627
628#if 0
629FlagsHandler::FlagsHandler(IMAPMailbox& mailbox)
630	:
631	IMAPMailboxCommand(mailbox)
632{
633}
634
635
636bool
637FlagsHandler::HandleUntagged(const BString& response)
638{
639	if (response.FindFirst("FETCH") < 0)
640		return false;
641
642	int32 fetch = 0;
643	if (!IMAPParser::ExtractUntagedFromLeft(response, "FETCH", fetch))
644		return false;
645
646	int32 flags = FetchMinMessageCommand::ExtractFlags(response);
647	int32 uid = fIMAPMailbox.MessageNumberToUID(fetch);
648	fStorage.SetFlags(uid, flags);
649	TRACE("FlagsHandler id %i flags %i\n", (int)fetch, (int)flags);
650	fIMAPMailbox.SendRawCommand("DONE");
651
652	return true;
653}
654#endif
655
656
657// #pragma mark -
658
659
660ListCommand::ListCommand(const char* prefix, bool subscribedOnly)
661	:
662	fPrefix(prefix),
663	fSubscribedOnly(subscribedOnly)
664{
665}
666
667
668BString
669ListCommand::CommandString()
670{
671	BString command = _Command();
672	command += " \"\" \"";
673	if (fPrefix != NULL)
674		command << fEncoding.Encode(fPrefix) << "%";
675	else
676		command += "*";
677	command += "\"";
678
679	return command;
680}
681
682
683bool
684ListCommand::HandleUntagged(Response& response)
685{
686	if (response.IsCommand(_Command()) && response.IsStringAt(2)
687		&& response.IsStringAt(3)) {
688		fSeparator = response.StringAt(2);
689
690		if (response.IsListAt(1)) {
691			// We're not supposed to select \Noselect mailboxes,
692			// so we'll just hide them
693			ArgumentList& attributes = response.ListAt(1);
694			if (attributes.Contains("\\Noselect"))
695				return true;
696		}
697
698		BString folder = response.StringAt(3);
699		if (folder == "")
700			return true;
701
702		try {
703			folder = fEncoding.Decode(folder);
704			// The folder INBOX is always case insensitive
705			if (folder.ICompare("INBOX") == 0)
706				folder = "Inbox";
707			fFolders.Add(folder);
708		} catch (ParseException& exception) {
709			// Decoding failed, just add the plain text
710			fprintf(stderr, "Decoding \"%s\" failed: %s\n", folder.String(),
711				exception.Message());
712			fFolders.Add(folder);
713		}
714		return true;
715	}
716
717	return false;
718}
719
720
721const BStringList&
722ListCommand::FolderList()
723{
724	return fFolders;
725}
726
727
728const char*
729ListCommand::_Command() const
730{
731	return fSubscribedOnly ? "LSUB" : "LIST";
732}
733
734
735// #pragma mark -
736
737
738SubscribeCommand::SubscribeCommand(const char* mailboxName)
739	:
740	fMailboxName(mailboxName)
741{
742}
743
744
745BString
746SubscribeCommand::CommandString()
747{
748	BString command = "SUBSCRIBE \"";
749	command += fMailboxName;
750	command += "\"";
751	return command;
752}
753
754
755// #pragma mark -
756
757
758UnsubscribeCommand::UnsubscribeCommand(const char* mailboxName)
759	:
760	fMailboxName(mailboxName)
761{
762}
763
764
765BString
766UnsubscribeCommand::CommandString()
767{
768	BString command = "UNSUBSCRIBE \"";
769	command += fMailboxName;
770	command += "\"";
771	return command;
772}
773
774
775// #pragma mark -
776
777
778GetQuotaCommand::GetQuotaCommand(const char* mailboxName)
779	:
780	fMailboxName(mailboxName),
781	fUsedStorage(0),
782	fTotalStorage(0)
783{
784}
785
786
787BString
788GetQuotaCommand::CommandString()
789{
790	BString command = "GETQUOTAROOT \"";
791	command += fMailboxName;
792	command += "\"";
793	return command;
794}
795
796
797bool
798GetQuotaCommand::HandleUntagged(Response& response)
799{
800	if (!response.IsCommand("QUOTA") || !response.IsListAt(2))
801		return false;
802
803	const ArgumentList& arguments = response.ListAt(2);
804	if (!arguments.EqualsAt(0, "STORAGE"))
805		return false;
806
807	fUsedStorage = (uint64)arguments.NumberAt(1) * 1024;
808	fTotalStorage = (uint64)arguments.NumberAt(2) * 1024;
809
810	return true;
811}
812
813
814uint64
815GetQuotaCommand::UsedStorage()
816{
817	return fUsedStorage;
818}
819
820
821uint64
822GetQuotaCommand::TotalStorage()
823{
824	return fTotalStorage;
825}
826
827
828}	// namespace
829