1/*
2 * Copyright 2010-2016, Haiku Inc. All Rights Reserved.
3 * Copyright 2010 Clemens Zeidler. All rights reserved.
4 *
5 * Distributed under the terms of the MIT License.
6 */
7
8
9#include "Protocol.h"
10
11#include "Commands.h"
12
13
14#define DEBUG_IMAP_PROTOCOL
15#ifdef DEBUG_IMAP_PROTOCOL
16#	include <stdio.h>
17#	define TRACE(...) printf(__VA_ARGS__)
18#else
19#	define TRACE(...) ;
20#endif
21
22
23namespace IMAP {
24
25
26Protocol::Protocol()
27	:
28	fSocket(NULL),
29	fBufferedSocket(NULL),
30	fHandlerList(5, false),
31	fCommandID(0),
32	fIsConnected(false)
33{
34}
35
36
37Protocol::~Protocol()
38{
39	delete fSocket;
40	delete fBufferedSocket;
41}
42
43
44status_t
45Protocol::Connect(const BNetworkAddress& address, const char* username,
46	const char* password, bool useSSL)
47{
48	TRACE("Connect\n");
49	if (useSSL)
50		fSocket = new(std::nothrow) BSecureSocket(address);
51	else
52		fSocket = new(std::nothrow) BSocket(address);
53
54	if (fSocket == NULL)
55		return B_NO_MEMORY;
56
57	status_t status = fSocket->InitCheck();
58	if (status != B_OK)
59		return status;
60
61	fBufferedSocket = new(std::nothrow) BBufferedDataIO(*fSocket, 32768, false,
62		true);
63	if (fBufferedSocket == NULL)
64		return B_NO_MEMORY;
65
66	TRACE("Login\n");
67
68	fIsConnected = true;
69
70	LoginCommand login(username, password);
71	status = ProcessCommand(login);
72	if (status != B_OK) {
73		_Disconnect();
74		return status;
75	}
76
77	_ParseCapabilities(login.Capabilities());
78
79	if (fCapabilities.IsEmpty()) {
80		CapabilityHandler capabilityHandler;
81		status = ProcessCommand(capabilityHandler);
82		if (status != B_OK)
83			return status;
84
85		_ParseCapabilities(capabilityHandler.Capabilities());
86	}
87
88	if (Capabilities().Contains("ID")) {
89		// Get the server's ID into our log
90		class IDCommand : public IMAP::Command, public IMAP::Handler {
91		public:
92			BString CommandString()
93			{
94				return "ID NIL";
95			}
96
97			bool HandleUntagged(IMAP::Response& response)
98			{
99				if (response.IsCommand("ID") && response.IsListAt(1)) {
100					puts("Server:");
101					ArgumentList& list = response.ListAt(1);
102					for (int32 i = 0; i < list.CountItems(); i += 2) {
103						printf("  %s: %s\n",
104							list.ItemAt(i)->ToString().String(),
105							list.ItemAt(i + 1)->ToString().String());
106					}
107					return true;
108				}
109
110				return false;
111			}
112		};
113		IDCommand idCommand;
114		ProcessCommand(idCommand);
115	}
116	return B_OK;
117}
118
119
120status_t
121Protocol::Disconnect()
122{
123	if (IsConnected()) {
124		RawCommand command("LOGOUT");
125		ProcessCommand(command);
126	}
127	return _Disconnect();
128}
129
130
131bool
132Protocol::IsConnected()
133{
134	return fIsConnected;
135}
136
137
138bool
139Protocol::AddHandler(Handler& handler)
140{
141	return fHandlerList.AddItem(&handler);
142}
143
144
145void
146Protocol::RemoveHandler(Handler& handler)
147{
148	fHandlerList.RemoveItem(&handler);
149}
150
151
152status_t
153Protocol::GetFolders(FolderList& folders, BString& separator)
154{
155	BStringList allFolders;
156	status_t status = _GetAllFolders(allFolders);
157	if (status != B_OK)
158		return status;
159
160	BStringList subscribedFolders;
161	status = GetSubscribedFolders(subscribedFolders, separator);
162	if (status != B_OK)
163		return status;
164
165	for (int32 i = 0; i < allFolders.CountStrings(); i++) {
166		FolderEntry entry;
167		entry.folder = allFolders.StringAt(i);
168		for (int32 j = 0; j < subscribedFolders.CountStrings(); j++) {
169			if (entry.folder == subscribedFolders.StringAt(j)) {
170				entry.subscribed = true;
171				break;
172			}
173		}
174		folders.push_back(entry);
175	}
176
177	// you could be subscribed to a folder which not exist currently, add them:
178	for (int32 i = 0; i < subscribedFolders.CountStrings(); i++) {
179		bool isInlist = false;
180		for (int32 j = 0; j < allFolders.CountStrings(); j++) {
181			if (subscribedFolders.StringAt(i) == allFolders.StringAt(j)) {
182				isInlist = true;
183				break;
184			}
185		}
186		if (isInlist)
187			continue;
188
189		FolderEntry entry;
190		entry.folder = subscribedFolders.StringAt(i);
191		entry.subscribed = true;
192		folders.push_back(entry);
193	}
194
195	return B_OK;
196}
197
198
199status_t
200Protocol::GetSubscribedFolders(BStringList& folders, BString& separator)
201{
202	ListCommand command(NULL, true);
203	status_t status = ProcessCommand(command);
204	if (status != B_OK)
205		return status;
206
207	folders = command.FolderList();
208	separator = command.Separator();
209	return status;
210}
211
212
213status_t
214Protocol::SubscribeFolder(const char* folder)
215{
216	SubscribeCommand command(folder);
217	return ProcessCommand(command);
218}
219
220
221status_t
222Protocol::UnsubscribeFolder(const char* folder)
223{
224	UnsubscribeCommand command(folder);
225	return ProcessCommand(command);
226}
227
228
229status_t
230Protocol::GetQuota(uint64& used, uint64& total)
231{
232	if (!Capabilities().Contains("QUOTA"))
233		return B_ERROR;
234
235	GetQuotaCommand quotaCommand;
236	status_t status = ProcessCommand(quotaCommand);
237	if (status != B_OK)
238		return status;
239
240	used = quotaCommand.UsedStorage();
241	total = quotaCommand.TotalStorage();
242	return B_OK;
243}
244
245
246status_t
247Protocol::SendCommand(const char* command)
248{
249	return SendCommand(0, command);
250}
251
252
253status_t
254Protocol::SendCommand(int32 id, const char* command)
255{
256	char buffer[2048];
257	int32 length;
258	if (id > 0) {
259		length = snprintf(buffer, sizeof(buffer), "A%.7" B_PRId32 " %s\r\n",
260			id, command);
261	} else
262		length = snprintf(buffer, sizeof(buffer), "%s\r\n", command);
263
264	TRACE("C: %s", buffer);
265
266	ssize_t bytesWritten = fSocket->Write(buffer, length);
267	if (bytesWritten < 0)
268		return bytesWritten;
269
270	return bytesWritten == length ? B_OK : B_ERROR;
271}
272
273
274ssize_t
275Protocol::SendData(const char* buffer, uint32 length)
276{
277	return fSocket->Write(buffer, length);
278}
279
280
281status_t
282Protocol::ProcessCommand(Command& command, bigtime_t timeout)
283{
284	BString commandString = command.CommandString();
285	if (commandString.IsEmpty())
286		return B_BAD_VALUE;
287
288	Handler* handler = dynamic_cast<Handler*>(&command);
289	if (handler != NULL && !AddHandler(*handler))
290		return B_NO_MEMORY;
291
292	int32 commandID = NextCommandID();
293	status_t status = SendCommand(commandID, commandString.String());
294	if (status == B_OK) {
295		fOngoingCommands[commandID] = &command;
296		status = HandleResponse(&command, timeout);
297	}
298
299	if (handler != NULL)
300		RemoveHandler(*handler);
301
302	return status;
303}
304
305
306// #pragma mark - protected
307
308
309status_t
310Protocol::HandleResponse(Command* command, bigtime_t timeout,
311	bool disconnectOnTimeout)
312{
313	status_t commandStatus = B_OK;
314	IMAP::ResponseParser parser(*fBufferedSocket);
315	if (IMAP::LiteralHandler* literalHandler
316			= dynamic_cast<IMAP::LiteralHandler*>(command))
317		parser.SetLiteralHandler(literalHandler);
318
319	IMAP::Response response;
320
321	bool done = false;
322	while (!done) {
323		try {
324			status_t status = parser.NextResponse(response, timeout);
325			if (status != B_OK) {
326				// we might have lost the connection, clear the connection state
327				if (status != B_TIMED_OUT || disconnectOnTimeout)
328					_Disconnect();
329
330				return status;
331			}
332
333			if (response.IsUntagged() || response.IsContinuation()) {
334				bool handled = false;
335				for (int32 i = fHandlerList.CountItems(); i-- > 0;) {
336					if (fHandlerList.ItemAt(i)->HandleUntagged(response)) {
337						handled = true;
338						break;
339					}
340				}
341				if (!handled)
342					printf("Unhandled S: %s\n", response.ToString().String());
343			} else {
344				CommandIDMap::iterator found
345					= fOngoingCommands.find(response.Tag());
346				if (found != fOngoingCommands.end()) {
347					status_t status = found->second->HandleTagged(response);
348					if (status != B_OK)
349						commandStatus = status;
350
351					fOngoingCommands.erase(found);
352				} else
353					printf("Unknown tag S: %s\n", response.ToString().String());
354			}
355		} catch (ParseException& exception) {
356			printf("Error during parsing: %s\n", exception.Message());
357		} catch (StreamException& exception) {
358			return exception.Status();
359		}
360
361		if (fOngoingCommands.size() == 0)
362			done = true;
363	}
364
365	return commandStatus;
366}
367
368
369int32
370Protocol::NextCommandID()
371{
372	fCommandID++;
373	return fCommandID;
374}
375
376
377// #pragma mark - private
378
379
380status_t
381Protocol::_Disconnect()
382{
383	fOngoingCommands.clear();
384	fIsConnected = false;
385	delete fBufferedSocket;
386	fBufferedSocket = NULL;
387	delete fSocket;
388	fSocket = NULL;
389
390	return B_OK;
391}
392
393
394status_t
395Protocol::_GetAllFolders(BStringList& folders)
396{
397	ListCommand command(NULL, false);
398	status_t status = ProcessCommand(command);
399	if (status != B_OK)
400		return status;
401
402	folders = command.FolderList();
403	return status;
404}
405
406
407void
408Protocol::_ParseCapabilities(const ArgumentList& arguments)
409{
410	fCapabilities.MakeEmpty();
411
412	for (int32 i = 0; i < arguments.CountItems(); i++) {
413		if (StringArgument* argument
414				= dynamic_cast<StringArgument*>(arguments.ItemAt(i)))
415			fCapabilities.AddItem(new StringArgument(*argument));
416	}
417
418	TRACE("capabilities: %s\n", fCapabilities.ToString().String());
419}
420
421
422}	// namespace IMAP
423