From 95564d826ed562d54ce7dc61e055dc716def428f Mon Sep 17 00:00:00 2001 From: David Rousselie Date: Thu, 11 Oct 2007 22:40:51 +0200 Subject: [PATCH] Browse IMAP folders in service discorvery (IMAP connection not yet implemented) darcs-hash:20071011204051-86b55-b2e67101f0396919e907fe98c42b9034bc3142d7.gz --- src/jmc/jabber/component.py | 74 +++++++- src/jmc/jabber/disco.py | 97 ++++++++++ src/jmc/jabber/tests/component.py | 290 +++++++++++++++++++++++++++++- 3 files changed, 454 insertions(+), 7 deletions(-) diff --git a/src/jmc/jabber/component.py b/src/jmc/jabber/component.py index cf7943e..1e1e38d 100644 --- a/src/jmc/jabber/component.py +++ b/src/jmc/jabber/component.py @@ -25,14 +25,22 @@ import logging from pyxmpp.jid import JID +from sqlobject.sqlbuilder import AND + import jcl.jabber as jabber +import jcl.model as model from jcl.model import account -from jcl.model.account import PresenceAccount -from jcl.jabber.disco import RootDiscoGetInfoHandler +from jcl.model.account import Account, User, PresenceAccount +from jcl.jabber.disco import \ + AccountTypeDiscoGetInfoHandler, AccountDiscoGetInfoHandler from jcl.jabber.feeder import FeederComponent, Feeder, MessageSender, \ HeadlineSender, FeederHandler +from jcl.jabber.command import CommandRootDiscoGetInfoHandler +from jcl.jabber.component import AccountManager -from jmc.jabber.disco import MailRootDiscoGetInfoHandler +from jmc.jabber.disco import MailRootDiscoGetInfoHandler, \ + IMAPAccountDiscoGetItemsHandler, MailAccountTypeDiscoGetInfoHandler, \ + IMAPAccountDiscoGetInfoHandler from jmc.jabber.message import SendMailMessageHandler, \ RootSendMailMessageHandler from jmc.jabber.presence import MailSubscribeHandler, \ @@ -41,6 +49,47 @@ from jmc.model.account import MailAccount, IMAPAccount, POP3Account, \ SMTPAccount from jmc.lang import Lang +class MailAccountManager(AccountManager): + def account_get_register(self, info_query, + name, + from_jid, + account_type, + lang_class): + """Handle get_register on an IMAP account. + Return a preinitialized form. + account_type contains 'account_type + imap_dir'""" + splitted_node = account_type.split("/") + splitted_node_len = len(splitted_node) + if splitted_node_len == 1 or \ + splitted_node[0] != "IMAP": + return AccountManager.account_get_register(self, + info_query, + name, from_jid, + account_type, + lang_class) + else: + info_query = info_query.make_result_response() + model.db_connect() + imap_dir = "/".join(splitted_node[1:]) + bare_from_jid = from_jid.bare() + _account = account.get_account_filter(\ + AND(AND(User.q.jid == unicode(bare_from_jid), + Account.q.userID == User.q.id), + IMAPAccount.q.mailbox == imap_dir), + account_class=IMAPAccount) + query = info_query.new_query("jabber:iq:register") + if _account is not None: + self.generate_registration_form_init(lang_class, + _account).as_xml(query) + else: + result = self.generate_registration_form(\ + lang_class, + IMAPAccount, + bare_from_jid) + result["mailbox"].value = imap_dir + result.as_xml(query) + return [info_query] + class MailComponent(FeederComponent): """Jabber Mail Component main implementation""" @@ -51,7 +100,8 @@ class MailComponent(FeederComponent): port, config, config_file, - lang=Lang()): + lang=Lang(), + account_manager_class=MailAccountManager): """Use FeederComponent behavior and setup feeder and sender attributes. """ @@ -62,7 +112,8 @@ class MailComponent(FeederComponent): port, config, config_file, - lang=lang) + lang=lang, + account_manager_class=account_manager_class) self.handler = MailFeederHandler(MailFeeder(self), MailSender(self)) self.account_manager.account_classes = (IMAPAccount, POP3Account, @@ -73,9 +124,20 @@ class MailComponent(FeederComponent): self.presence_unsubscribe_handlers += [[MailUnsubscribeHandler(self)]] self.presence_available_handlers += [[MailPresenceHandler(self)]] self.presence_unavailable_handlers += [[MailPresenceHandler(self)]] + self.disco_get_items_handlers[0] += [IMAPAccountDiscoGetItemsHandler(self)] jabber.replace_handlers(self.disco_get_info_handlers, - RootDiscoGetInfoHandler, + CommandRootDiscoGetInfoHandler, MailRootDiscoGetInfoHandler(self)) + jabber.replace_handlers(self.disco_get_info_handlers, + AccountTypeDiscoGetInfoHandler, + MailAccountTypeDiscoGetInfoHandler(self)) + jabber.replace_handlers(self.disco_get_info_handlers, + AccountDiscoGetInfoHandler, + IMAPAccountDiscoGetInfoHandler(self)) +# for hg in self.disco_get_items_handlers: +# print "----" +# for h in hg: +# print str(h) class MailFeeder(Feeder): """Email check""" diff --git a/src/jmc/jabber/disco.py b/src/jmc/jabber/disco.py index 0e9fe4e..3ba004e 100644 --- a/src/jmc/jabber/disco.py +++ b/src/jmc/jabber/disco.py @@ -20,7 +20,18 @@ ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## +import logging + +from pyxmpp.jid import JID +from pyxmpp.jabber.disco import DiscoItems, DiscoItem + +from jcl import jabber +from jcl.model import account from jcl.jabber.command import CommandRootDiscoGetInfoHandler +from jcl.jabber.disco import AccountTypeDiscoGetInfoHandler, \ + AccountDiscoGetInfoHandler, DiscoHandler + +from jmc.model.account import IMAPAccount class MailRootDiscoGetInfoHandler(CommandRootDiscoGetInfoHandler): def handle(self, stanza, lang_class, node, disco_obj, data): @@ -30,3 +41,89 @@ class MailRootDiscoGetInfoHandler(CommandRootDiscoGetInfoHandler): disco_infos[0].add_feature("jabber:iq:gateway") disco_infos[0].add_identity(self.component.name, "headline", "newmail") return disco_infos + +class MailAccountTypeDiscoGetInfoHandler(AccountTypeDiscoGetInfoHandler): + def handle(self, stanza, lang_class, node, disco_obj, data): + disco_infos = AccountTypeDiscoGetInfoHandler.handle(self, stanza, + lang_class, + node, disco_obj, + data) + if node == "IMAP" and account.get_accounts(stanza.get_from().bare(), + IMAPAccount).count() > 0: + disco_infos[0].add_feature("http://jabber.org/protocol/disco#info") + disco_infos[0].add_feature("http://jabber.org/protocol/disco#items") + return disco_infos + +class IMAPAccountDiscoGetInfoHandler(AccountDiscoGetInfoHandler): + def handle(self, stanza, lang_class, node, disco_obj, data): + disco_infos = AccountDiscoGetInfoHandler.handle(self, stanza, + lang_class, + node, disco_obj, + data) + splitted_node = node.split("/") + splitted_node_len = len(splitted_node) + if splitted_node_len > 1 and \ + splitted_node[0] == "IMAP": + _account = account.get_account(stanza.get_from().bare(), + splitted_node[1], + IMAPAccount) + if _account is not None: + disco_infos[0].add_feature("http://jabber.org/protocol/disco#info") + if splitted_node_len > 2: + imap_dir = "/".join(splitted_node[2:]) + if len(_account.ls_dir(imap_dir)) > 0: + disco_infos[0].add_feature("http://jabber.org/protocol/disco#items") + else: + disco_infos[0].add_feature("http://jabber.org/protocol/disco#items") + return disco_infos + +class IMAPAccountDiscoGetItemsHandler(DiscoHandler): + def __init__(self, component): + DiscoHandler.__init__(self, component) + self.__logger = logging.getLogger("jcl.jabber.IMAPAccountDiscoGetItemsHandler") + + def filter(self, stanza, lang_class, node=None): + if node is None: + return None + splitted_node = node.split("/") + splitted_node_len = len(splitted_node) + if splitted_node_len < 2: + return None + account_type = splitted_node[0] + if account_type != "IMAP": + return None + account_name = splitted_node[1] + if splitted_node_len == 2: + imap_dir = None + else: + imap_dir = "/".join(splitted_node[2:]) + return (account_type, account_name, imap_dir) + + def handle(self, stanza, lang_class, node, disco_obj, data): + """Discovery get_items on an IMAP account""" + account_type, account_name, imap_dir = data + bare_from_jid = stanza.get_from().bare() + self.__logger.debug("Listing " + str(imap_dir) + " IMAP directory") + account_class = self.component.account_manager.get_account_class(account_type) + _account = account.get_account(bare_from_jid, account_name, + account_class=account_class) + if account_class is not None and _account is not None: + disco_items = DiscoItems() + if imap_dir is None: + # TODO : test if INBOX really exist, is it possible to retrieve default dir from IMAP ? + DiscoItem(disco_items, + JID(unicode(_account.jid) + "/" + account_type + + "/INBOX"), + account_type + "/" + account_name + "/INBOX", + "INBOX") + else: + for subdir in _account.ls_dir(imap_dir): + DiscoItem(disco_items, + JID(unicode(_account.jid) + "/" + account_type + \ + "/" + imap_dir + "/" + subdir), + account_type + "/" + account_name + "/" + imap_dir + + "/" + subdir, + subdir) + return [disco_items] + return [] +# TODO : implement get_info on imap_Dir diff --git a/src/jmc/jabber/tests/component.py b/src/jmc/jabber/tests/component.py index 8b5ed57..88893af 100644 --- a/src/jmc/jabber/tests/component.py +++ b/src/jmc/jabber/tests/component.py @@ -31,6 +31,7 @@ from sqlobject.dbconnection import TheURIOpener from pyxmpp.presence import Presence from pyxmpp.message import Message +from pyxmpp.iq import Iq from jcl.tests import JCLTestCase import jcl.model as model @@ -143,9 +144,16 @@ class MockIMAPAccount(MockMailAccount, IMAPAccount): IMAPAccount._init(self, *args, **kw) MockMailAccount._init(self) + def ls_dir(self, imap_dir): + if imap_dir == "INBOX": + return ["dir1", "dir2"] + elif imap_dir == "INBOX/dir1": + return ["subdir1", "subdir2"] + return [] + class MockPOP3Account(MockMailAccount, POP3Account): def _init(self, *args, **kw): - IMAPAccount._init(self, *args, **kw) + POP3Account._init(self, *args, **kw) MockMailAccount._init(self) class MockSMTPAccount(object): @@ -514,6 +522,286 @@ class MailComponent_TestCase(JCLTestCase): self.assertTrue(account11.marked_all_as_read) model.db_disconnect() + def test_disco_get_info_imap_node(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="jcl.test.com/IMAP") + disco_info = self.comp.disco_get_info("IMAP", info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_imap_node_no_account(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockPOP3Account(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + account21 = MockIMAPAccount(user=User(jid="user2@test.com"), + name="account21", + jid="account21@jmc.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="jcl.test.com/IMAP") + disco_info = self.comp.disco_get_info("IMAP", info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertFalse(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertFalse(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_imap_long_node(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP") + disco_info = self.comp.disco_get_info("IMAP/account11", + info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_not_imap_long_node(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockPOP3Account(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/POP3") + disco_info = self.comp.disco_get_info("POP3/account11", + info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertFalse(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertFalse(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_imap_dir_node(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + account11.mailbox = "INBOX/dir1" + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP/INBOX") + disco_info = self.comp.disco_get_info("IMAP/account11/INBOX", + info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_imap_dir_node_already_registered(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + account11.mailbox = "INBOX" + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP/INBOX") + disco_info = self.comp.disco_get_info("IMAP/account11/INBOX", + info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_info_imap_dir_node_last_subdir(self): + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jmc.test.com") + account11.mailbox = "INBOX/dir1/subdir1" + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP/INBOX/dir1/subdir1") + disco_info = self.comp.disco_get_info("IMAP/account11/INBOX/dir1/subdir1", + info_query) + self.assertEquals(len(self.comp.stream.sent), 0) + self.assertTrue(disco_info.has_feature("jabber:iq:register")) + self.assertTrue(disco_info.has_feature("http://jabber.org/protocol/disco#info")) + self.assertFalse(disco_info.has_feature("http://jabber.org/protocol/disco#items")) + + def test_disco_get_items_base_imap_account(self): + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jcl.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP") + disco_items = self.comp.disco_get_items("IMAP/account11", info_query) + self.assertEquals(len(disco_items.get_items()), 1) + disco_item = disco_items.get_items()[0] + self.assertEquals(unicode(disco_item.get_jid()), unicode(account11.jid) + + "/IMAP/INBOX") + self.assertEquals(disco_item.get_node(), "IMAP/" + account11.name + "/INBOX") + self.assertEquals(disco_item.get_name(), "INBOX") + + def test_disco_get_items_inbox_imap_account(self): + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jcl.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP/INBOX") + disco_items = self.comp.disco_get_items("IMAP/account11/INBOX", info_query) + self.assertEquals(len(disco_items.get_items()), 2) + disco_item = disco_items.get_items()[0] + self.assertEquals(unicode(disco_item.get_jid()), unicode(account11.jid) + + "/IMAP/INBOX/dir1") + self.assertEquals(disco_item.get_node(), "IMAP/" + account11.name + "/INBOX" + + "/dir1") + self.assertEquals(disco_item.get_name(), "dir1") + disco_item = disco_items.get_items()[1] + self.assertEquals(unicode(disco_item.get_jid()), unicode(account11.jid) + + "/IMAP/INBOX/dir2") + self.assertEquals(disco_item.get_node(), "IMAP/" + account11.name + "/INBOX" + + "/dir2") + self.assertEquals(disco_item.get_name(), "dir2") + + def test_disco_get_items_subdir_imap_account(self): + account11 = MockIMAPAccount(user=User(jid="user1@test.com"), + name="account11", + jid="account11@jcl.test.com") + info_query = Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account11@jcl.test.com/IMAP") + disco_items = self.comp.disco_get_items("IMAP/account11/INBOX/dir1", info_query) + self.assertEquals(len(disco_items.get_items()), 2) + disco_item = disco_items.get_items()[0] + self.assertEquals(unicode(disco_item.get_jid()), unicode(account11.jid) + + "/IMAP/INBOX/dir1/subdir1") + self.assertEquals(disco_item.get_node(), "IMAP/" + account11.name + "/INBOX" + + "/dir1/subdir1") + self.assertEquals(disco_item.get_name(), "subdir1") + disco_item = disco_items.get_items()[1] + self.assertEquals(unicode(disco_item.get_jid()), unicode(account11.jid) + + "/IMAP/INBOX/dir1/subdir2") + self.assertEquals(disco_item.get_node(), "IMAP/" + account11.name + "/INBOX" + + "/dir1/subdir2") + self.assertEquals(disco_item.get_name(), "subdir2") + + def test_account_get_register_imap_dir_already_registered(self): + model.db_connect() + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + user1 = User(jid="user1@test.com") + account1 = MockIMAPAccount(user=user1, + name="account1", + jid="account1@jcl.test.com") + account1.mailbox = "INBOX" + account11 = MockIMAPAccount(user=user1, + name="account11", + jid="account11@jcl.test.com") + account11.mailbox = "INBOX/dir1" + account21 = MockIMAPAccount(user=User(jid="user2@test.com"), + name="account21", + jid="account21@jcl.test.com") + model.db_disconnect() + self.comp.handle_get_register(Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account1@jcl.test.com/IMAP/INBOX/dir1")) + self.assertEquals(len(self.comp.stream.sent), 1) + iq_sent = self.comp.stream.sent[0] + self.assertEquals(iq_sent.get_to(), "user1@test.com") + titles = iq_sent.xpath_eval("jir:query/jxd:x/jxd:title", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(titles), 1) + self.assertEquals(titles[0].content, + Lang.en.register_title) + instructions = iq_sent.xpath_eval("jir:query/jxd:x/jxd:instructions", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(instructions), 1) + self.assertEquals(instructions[0].content, + Lang.en.register_instructions) + fields = iq_sent.xpath_eval("jir:query/jxd:x/jxd:field", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(fields), 16) + field = fields[0] + self.assertEquals(field.prop("type"), "hidden") + self.assertEquals(field.prop("var"), "name") + self.assertEquals(field.prop("label"), Lang.en.account_name) + self.assertEquals(field.children.name, "value") + self.assertEquals(field.children.content, "account11") + self.assertEquals(field.children.next.name, "required") + field = fields[15] + self.assertEquals(field.prop("type"), "text-single") + self.assertEquals(field.prop("var"), "mailbox") + self.assertEquals(field.prop("label"), Lang.en.field_mailbox) + self.assertEquals(field.children.name, "value") + self.assertEquals(field.children.content, "INBOX/dir1") + + def test_account_get_register_imap_dir_new(self): + model.db_connect() + self.comp.stream = MockStream() + self.comp.stream_class = MockStream + user1 = User(jid="user1@test.com") + account1 = MockIMAPAccount(user=user1, + name="account1", + jid="account1@jcl.test.com") + account1.maildir = "INBOX" + account11 = MockIMAPAccount(user=user1, + name="account11", + jid="account11@jcl.test.com") + account11.maildir = "INBOX/dir1" + account21 = MockIMAPAccount(user=User(jid="user2@test.com"), + name="account21", + jid="account21@jcl.test.com") + model.db_disconnect() + self.comp.handle_get_register(Iq(stanza_type="get", + from_jid="user1@test.com", + to_jid="account1@jcl.test.com/IMAP/INBOX/dir1/subdir1")) + self.assertEquals(len(self.comp.stream.sent), 1) + iq_sent = self.comp.stream.sent[0] + self.assertEquals(iq_sent.get_to(), "user1@test.com") + titles = iq_sent.xpath_eval("jir:query/jxd:x/jxd:title", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(titles), 1) + self.assertEquals(titles[0].content, + Lang.en.register_title) + instructions = iq_sent.xpath_eval("jir:query/jxd:x/jxd:instructions", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(instructions), 1) + self.assertEquals(instructions[0].content, + Lang.en.register_instructions) + fields = iq_sent.xpath_eval("jir:query/jxd:x/jxd:field", + {"jir" : "jabber:iq:register", + "jxd" : "jabber:x:data"}) + self.assertEquals(len(fields), 16) + field = fields[0] + self.assertEquals(field.prop("type"), "text-single") + self.assertEquals(field.prop("var"), "name") + self.assertEquals(field.prop("label"), Lang.en.account_name) + self.assertEquals(field.children.name, "required") + self.assertEquals(field.children.next, None) + field = fields[15] + self.assertEquals(field.prop("type"), "text-single") + self.assertEquals(field.prop("var"), "mailbox") + self.assertEquals(field.prop("label"), Lang.en.field_mailbox) + self.assertEquals(field.children.name, "value") + self.assertEquals(field.children.content, "INBOX/dir1/subdir1") + class SendMailMessageHandler_TestCase(unittest.TestCase): def setUp(self): self.handler = SendMailMessageHandler(None)