1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * Authors : Patrick Patterson <ppatters@nit.ca>
4 * Peter Colijn <pcolijn@nit.ca>
5 * Scott MacLean <scott@nit.ca>
6 * William Lachance <wlach@nit.ca>
7 * Hubert Figuiere <hub@nit.ca>
9 * Copyright 2003-2004, Net Integration Technologies, Inc.
11 * This program is free software; you can redistribute it and/or
12 * modify it under the terms of version 2 of the GNU General Public
13 * License as published by the Free Software Foundation.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
26 #include "wvexcconn.h"
27 #include "wvunixsocket.h"
28 #include "wvtimeoutstream.h"
29 #include "wvexcconnmanager.h"
35 #include "wvsslstream.h"
36 #include "wvstringlist.h"
37 #include "wvencoderstream.h"
38 #include "wvautorelease.h"
39 #include "createfolder.h"
40 #include "deletefolder.h"
41 #include "listfolders.h"
42 #include "listusers.h"
48 #include "eituniconfkeys.h"
51 static int wvstrcmp(const WvString
*l
, const WvString
*r
)
53 return strcmp(*l
, *r
);
56 WvExcConn::WvExcConn(WvStream
*conn
, UniConf _config
,
57 WvExcConnManager
*_conn_manager
,
58 ExchangeItStorage
*_storage
,
59 const int _protocol_version
) :
61 log(WvString("WvExcConn-%s", (long)this), WvLog::Debug1
),
65 transaction_complete(false),
67 have_progress_cb(false),
69 conn_manager(_conn_manager
),
73 protocol_version(_protocol_version
)
76 uses_continue_select
= true;
81 setclosecallback(IWvStreamCallback(conn_manager
,
82 &WvExcConnManager::conn_closed
));
85 log("Connecting using protocol version %s\n",
89 // CB almost immediately to authenticate and do initial folder sync
90 // FIXME: is this the best way of doing this?
93 WvExcConn::~WvExcConn()
96 terminate_continue_select();
101 log(WvLog::Error
, "Aborted. Error was: %s\n", errstr());
102 if (!transaction_complete
&& sock
)
104 uint32_t failure
= 65535;
105 sock
->write(&failure
, sizeof(uint32_t));
109 log(WvLog::Info
, "Done.\n");
114 // Notify our camel provider of the progress we're making
115 void WvExcConn::progress_notify(int percent
)
117 if (!have_progress_cb
)
120 if (progress
< 0 || progress
> 100)
126 progress_cb(progress
);
129 ErrorType
WvExcConn::authenticate()
131 ErrorType result
= AuthOK
;
132 log("authenticate()\n");
134 WvString username
= config
[USERNAME_KEY
].getme();
135 WvString password
= config
[PASSWORD_KEY
].getme();
136 WvString serverid
= config
[SERVERID_KEY
].getme();
138 // we are compatible with both versions 2 (3.76) and
139 // 3 (3.80) of the exchangeit protocol
140 WvString
auth("%s:%s", username
, password
);
141 WvString authenc
= WvBase64Encoder().strflushstr(auth
, true);
143 // Prod the server with the Auth Information..
144 // We pick up the answer the first time into execute()
145 print("Version: %s\n", protocol_version
);
146 log(">>> (Version: %s)\n", protocol_version
);
147 print("Authenticate: %s\n\n", authenc
);
148 log(">>> (Authenticate: %s)\n", authenc
);
152 if (result
== AuthOK
)
156 log("Sorry, cannot even start authentication.\n");
162 // FIXME: Ideally want something more robust than a timeout
164 while(!!(line
= blocking_getline(AUTH_TIMEOUT
)))
166 log("<< (%s)\n", line
);
167 if (line
.len() >= 11 && !strncmp(line
, "* ERROR 401", 11))
170 seterr("Authentication failed");
174 if (line
.len() >= 7 && !strncmp(line
, "* ERROR", 7))
176 result
= ProtocolError
;
177 seterr("Response bad: %s", line
);
181 // actually this is dangerous as I'm not sure if headers are case sensitive
182 // or not. Probably safer to use strnicmp(). See bug 7277
183 if (!strncmp(line
, "Server-ID:", 10))
186 parts
.split(line
, " ");
188 if (parts
.count() != 2)
190 result
= ProtocolError
;
191 log("Wow, that's a messed up Server-Id header:\n%s\n",
193 seterr("Response bad: invalid Server-Id header");
198 WvString newid
= parts
.popstr();
200 if (!!serverid
&& newid
!= serverid
)
202 log("First time connecting to a new server; migrating.\n");
203 config
[OLD_SERVERID_KEY
].setme(serverid
);
204 config
[SERVERID_KEY
].setme(newid
);
215 if (result
== AuthOK
)
219 log("Sorry, cannot continue after authentication.\n");
224 conn_manager
->save_protocol_version(protocol_version
);
225 log("No obvious errors in response (so far)\n");
229 // returns true on success, false on (non-critical) failure
230 bool WvExcConn::migrate_account()
232 log("Detected change in account info. Migrating account information.\n");
234 if (!storage
->migrate_account(config
[USERNAME_KEY
].getme()))
237 if (config
[OLD_USERNAME_KEY
].exists())
238 config
[OLD_USERNAME_KEY
].remove();
240 if (config
[OLD_SERVERID_KEY
].exists())
241 config
[OLD_SERVERID_KEY
].remove();
246 bool WvExcConn::initial_folder_sync()
249 if (!apply_pending_responses(false))
252 // Create a toplevel "Calendar", "Contacts", and "Tasks" folders,
253 // if these don't exist already
254 WvString
calendar_folder("%s.Calendar", storage
->get_owner());
255 if (!config
[FOLDERS_KEY
][calendar_folder
].exists())
256 storage
->add_folder(calendar_folder
, EXCHANGEIT_CALENDAR_TYPE
, true);
258 WvString
contacts_folder("%s.Contacts", storage
->get_owner());
259 if (!config
[FOLDERS_KEY
][contacts_folder
].exists())
260 storage
->add_folder(contacts_folder
, EXCHANGEIT_CONTACTS_TYPE
, true);
262 WvString
tasks_folder("%s.Tasks", storage
->get_owner());
263 if (!config
[FOLDERS_KEY
][tasks_folder
].exists())
264 storage
->add_folder(tasks_folder
, EXCHANGEIT_TASKS_TYPE
, true);
269 void WvExcConn::send_folder_commands()
272 send_createfolders();
273 send_deletefolders();
277 // Here we send all the commands needed for a full sync of all folders.
278 // We do not parse/apply responses.
279 void WvExcConn::send_sync_commands()
281 // We were disconnected; retry remaining commands
282 CommandHash
*commands
= conn_manager
->get_commands();
283 WvAutorelease
<CommandHash
> autoreleaser(commands
);
285 log("send_sync_commands(): have progress_cb %s\n", have_progress_cb
);
287 if (commands
->count())
289 CommandHash::Iter
i(*commands
);
291 int total_cmds
= commands
->count();
293 log("Found %s unfinished commands. Retrying them.\n",
296 for (i
.rewind(); i
.next(); )
298 send_command(i().data
, i().key
);
299 progress_notify(50 * curr_cmd
++ / total_cmds
);
302 // We're good; do a full normal sync
305 // The progress indications here are pretty cheesy;
306 // is anybody going to care though?
322 // Here we read from the stream, and apply any responses we receive.
323 // (commands read the data they need now, not us)
324 bool WvExcConn::apply_pending_responses(bool show_progress
)
326 CommandHash
*commands
= conn_manager
->get_commands();
327 WvAutorelease
<CommandHash
> autoreleaser(commands
);
328 log("apply_pending_responses\n");
332 log("Command hash is gone; unable to apply repsonses\n");
338 int total_cmds
= commands
->count();
339 TaggedCommand
*tagged_command
;
345 // We call consume_newlines() at the end of each iteration to
346 // make this loop work.
347 while (!!(response
= blocking_getline(TIMEOUT
)))
349 log("<< (%s)\n", response
);
351 parts
.split(response
, " ");
352 tag
= parts
.popstr();
353 tagged_command
= NULL
;
355 log("Applying response for command %s\n", tag
);
359 command
= parts
.popstr();
360 GenericCommand
*generic_command
=
361 GenericCommand::get_command(command
);
363 if (!generic_command
)
364 log("Didn't get a command for %s!\n", command
);
366 if (!generic_command
||
367 !generic_command
->apply_response(parts
, *this, storage
))
368 log("Applying generic command %s failed!\n", command
);
370 delete generic_command
;
375 if (commands
->exists(cmd_num
))
377 log("Found command %s in table\n", cmd_num
);
378 tagged_command
= *commands
->find(cmd_num
);
381 if (tagged_command
&&
382 tagged_command
->apply_response(parts
, *this, storage
))
383 commands
->remove(cmd_num
);
385 log("Applying response to command %s failed!\n", tag
);
389 progress_notify(50 + (50 * curr_cmd
++ / total_cmds
));
393 if (commands
->isempty() || (curr_cmd
-1) == total_cmds
)
401 config
.commit(); // commit the results of this iteration
404 log("%s commands left after application\n", commands
->count());
407 // This is important, because it tells our camel "provider"
408 // to complete its operation
411 progress_notify(100);
413 bool isempty
= commands
->isempty();
417 void WvExcConn::send_listusers()
419 send_command(new ListUsers(storage
->get_state()[USERS_KEY
]));
422 template <class Command
>
423 void WvExcConn::send_acl_cmds(UniConf cfg
)
425 UniConf::Iter
i(cfg
);
426 Command
*command
= NULL
;
430 for (i
.rewind(); i
.next(); )
432 folder
= i().key().printable();
433 if (storage
->get_dict()[folder
])
434 type
= *storage
->get_dict()[folder
]->get_type();
436 type
= "UNKNOWN"; // uh-oh...
438 command
= new Command(folder
, type
, storage
);
439 send_command(command
);
444 void WvExcConn::send_setacls()
446 send_acl_cmds
<SetACL
>(storage
->get_state()[SETACL_KEY
]);
449 void WvExcConn::send_getacls()
451 send_acl_cmds
<GetACL
>(storage
->get_state()[GETACL_KEY
]);
454 void WvExcConn::send_createfolders()
456 CreateFolder
*create_folder
= NULL
;
459 WvStringList folders_toadd
;
461 UniConf folders_toadd_key
= storage
->get_state()[FOLDERS_KEY
][TOADD_KEY
];
462 UniConf::Iter
i(folders_toadd_key
);
463 for (i
.rewind(); i
.next();)
464 folders_toadd
.append(i().key().printable());
466 // Create folders in sorted order so that nested folders get created
468 WvStringList::Sorter
s(folders_toadd
, &::wvstrcmp
);
469 for (s
.rewind(); s
.next(); )
472 type
= folders_toadd_key
[name
].getme();
474 log("Folder add. Name: %s Type: %s\n", name
, type
);
476 create_folder
= new CreateFolder(name
, type
);
477 send_command(create_folder
);
478 create_folder
= NULL
;
482 void WvExcConn::send_deletefolders()
484 DeleteFolder
*delete_folder
= NULL
;
487 WvStringList folders_todelete
;
489 UniConf folders_todelete_key
= storage
->get_state()[FOLDERS_KEY
][TODELETE_KEY
];
490 UniConf::Iter
j(folders_todelete_key
);
491 for (j
.rewind(); j
.next(); )
493 log("Folder delete. Name: %s\n", j().key().printable());
495 delete_folder
= new DeleteFolder(j().key().printable(), j().getme());
496 send_command(delete_folder
);
497 delete_folder
= NULL
;
499 // Unconditionally remove the 'todelete' key: If we didn't succeed,
500 // the folder should be recreated the next time we sync
501 folders_todelete
.append(j().key().printable());
504 WvStringList::Iter
i(folders_todelete
);
505 for (i
.rewind(); i
.next(); )
506 folders_todelete_key
[i()].remove();
509 void WvExcConn::send_listfolders()
511 send_command(new ListFolders(storage
));
514 template <class Command
>
515 void WvExcConn::send_item_cmds(WvStringParm action
)
517 Command
*command
= NULL
;
519 ExchangeItAdaptorDict::Iter dict
= storage
->get_dict();
520 for (dict
.rewind(); dict
.next(); )
522 if (dict().is_ready())
524 if (dict().get_may_have_local_only_items())
525 dict().sync_local_items();
527 WvStringList
*item_list
= dict().get_item_uids(action
);
528 log("Found %s items in %s for folder %s\n",
529 item_list
->count(), action
, dict().key
);
531 WvStringList::Iter
i(*item_list
);
532 for (i
.rewind(); i
.next();)
534 log("Item %s for folder %s: %s\n", action
, dict().key
, i());
536 command
= new Command(&dict(), i());
537 send_command(command
);
546 void WvExcConn::send_deletes()
548 send_item_cmds
<Delete
>("todelete");
551 void WvExcConn::send_updates()
553 send_item_cmds
<Update
>("tosend");
556 void WvExcConn::send_syncs()
558 Sync
*sync_command
= NULL
;
560 ExchangeItAdaptorDict::Iter dict
= storage
->get_dict();
561 for (dict
.rewind(); dict
.next(); )
563 if (dict().is_ready())
565 sync_command
= new Sync(&dict());
566 send_command(sync_command
);
573 @param tagged_command it deleted by the callee !
575 void WvExcConn::send_command(TaggedCommand
*tagged_command
, int tag
)
578 log("Got a NULL command to send!\n");
580 CommandHash
*commands
= conn_manager
->get_commands();
581 WvAutorelease
<CommandHash
> autoreleaser(commands
);
584 log("Command hash is gone; unable to send commands\n");
586 // Check if the user tried to cancel the sync
588 if (sock
&& sizeof(uint32_t) == sock
->read(&msg
, sizeof(uint32_t)))
589 if (!msg
) nosend
= true;
591 if (nosend
|| !tagged_command
|| !commands
)
593 delete tagged_command
;
599 WvString server_command
;
603 tagged_command
->get_command(command
, params
, lines
);
605 // Tag is only to be set for commands being retried
608 log("Retrying a %s command\n", command
);
609 server_command
.append("%s %s", tag
, command
);
612 server_command
.append("%s %s", cmd_tag
, command
);
615 server_command
.append(" %s", params
.join(" "));
617 print("%s\n", server_command
);
618 log(">>> (%s)\n", server_command
);
620 while (lines
.count())
622 line
= lines
.popstr();
624 log(">>> (%s)\n", line
);
627 if (tagged_command
->has_data())
628 tagged_command
->send_data(*this);
632 // Only insert command if we're not retrying
634 commands
->add(cmd_tag
++, tagged_command
, true);
637 // Silly function to eat remaining blank lines from the stream. Helps us
638 // work around borken servers, and simplifies apply_resonse() functions.
639 void WvExcConn::consume_newlines()
649 if (c
!= '\n' && c
!= '\r')
657 log("Found %s newlines and ate them\n", num
);
658 unread(buf
, buf
.used());
661 bool WvExcConn::start_sync_cb()
675 void WvExcConn::cancel_sync_cb()
677 log("cancel_sync_cb\n");
679 have_progress_cb
= false;
682 void WvExcConn::unlink_commands()
684 // FIXME: this used to invalidate the commands member, but now
685 // that this is moved to WcExcConnManager, this is made useless
686 // I don't really know we should do.
687 terminate_continue_select();
690 bool WvExcConn::pre_select(SelectInfo
&si
)
692 bool ret
= WvStreamClone::pre_select(si
);
693 bool want_close
= false;
695 progress_notify(); // this will talk to camel,
696 // which should let us know if we need to quit or not
698 // If we've tried too many times OR the user cancels the sync, then reconnect
699 if (num_retries
>= MAX_RETRIES
|| nosend
)
705 // Unless we've completed a transaction, or we're still
707 if (transaction_complete
|| state
== Authenticate
)
710 transaction_complete
= false;
717 seterr("Failed to apply pending responses");
718 terminate_continue_select();
726 void WvExcConn::execute()
728 WvStreamClone::execute();
730 CommandHash
*commands
= conn_manager
->get_commands();
731 WvAutorelease
<CommandHash
> autoreleaser(commands
);
735 log("Command hash is gone; not doing anything.\n");
739 // we have to authenticate before we can do anything else
740 if (state
== Authenticate
)
742 ErrorType auth_result
;
744 log("Authenticating\n");
745 auth_result
= authenticate();
746 if (auth_result
!= AuthOK
)
748 // assume the server's gone and try again later
749 // FIXME: should tell the user, and actually differentiate
750 // between connection problems and authentication problems
751 set_errcode(auth_result
);
752 log("Authentication failed !!\n");
756 storage
->load_folders(); // load (cached) folder list into memory
757 if (commands
->count())
760 state
= InitialFolderSync
;
763 if (state
== InitialFolderSync
)
765 if (!!config
[OLD_USERNAME_KEY
].getme() || !!config
[OLD_SERVERID_KEY
].getme())
767 if (!migrate_account())
769 // call back later: not ready
774 log("Syncing folders for the first time!\n");
775 if (!initial_folder_sync())
778 seterr("Couldn't perform initial folder sync");
784 if (state
== Syncing
)
788 if (commands
->isempty())
790 send_folder_commands();
791 apply_pending_responses(false);
794 send_sync_commands();
795 bool success
= apply_pending_responses();
797 if (!transaction_complete
)
798 transaction_complete
= success
;