comparison src/plugins/plugin_misc_imap.py @ 257:012c38b56cdd

plugin IMAP, plugin Maildir: profile management - IMAP Login/pass is now checked against profile name/jabber pass - Mailboxes are now per-profile - flags are now checked without case sensitiveness
author Goffi <goffi@goffi.org>
date Tue, 18 Jan 2011 00:57:26 +0100
parents 55b750017b71
children c8406fe5e81e
comparison
equal deleted inserted replaced
256:f5181f6dd98f 257:012c38b56cdd
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 """ 4 """
5 SAT plugin for managing imap server 5 SàT plugin for managing imap server
6 Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) 6 Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org)
7 7
8 This program is free software: you can redistribute it and/or modify 8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by 9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or 10 the Free Software Foundation, either version 3 of the License, or
19 along with this program. If not, see <http://www.gnu.org/licenses/>. 19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """ 20 """
21 21
22 from logging import debug, info, error 22 from logging import debug, info, error
23 import warnings 23 import warnings
24 from twisted.internet import protocol 24 from twisted.internet import protocol,defer
25 from twisted.words.protocols.jabber import error as jab_error 25 from twisted.words.protocols.jabber import error as jab_error
26 from twisted.cred import portal,checkers 26 from twisted.cred import portal,checkers,credentials
27 from twisted.cred import error as cred_error
27 from twisted.mail import imap4 28 from twisted.mail import imap4
28 from email.parser import Parser 29 from email.parser import Parser
29 import email.message 30 import email.message
30 import os,os.path 31 import os,os.path
31 from cStringIO import StringIO 32 from cStringIO import StringIO
84 85
85 def getUID(self): 86 def getUID(self):
86 """Retrieve the unique identifier associated with this message. 87 """Retrieve the unique identifier associated with this message.
87 """ 88 """
88 debug('getUID (message)') 89 debug('getUID (message)')
89 debug ('===>%i', self.uid)
90 return self.uid 90 return self.uid
91 91
92 def getFlags(self): 92 def getFlags(self):
93 """Retrieve the flags associated with this message. 93 """Retrieve the flags associated with this message.
94 @return: The flags, represented as strings. 94 @return: The flags, represented as strings.
150 150
151 151
152 class SatMailbox: 152 class SatMailbox:
153 implements(imap4.IMailbox) 153 implements(imap4.IMailbox)
154 154
155 def __init__(self,host,name): 155 def __init__(self,host,name,profile):
156 self.host = host 156 self.host = host
157 self.listeners=set() 157 self.listeners=set()
158 debug ('Mailbox init (%s)', name) 158 debug ('Mailbox init (%s)', name)
159 if name!="INBOX": 159 if name!="INBOX":
160 raise imap4.MailboxException("Only INBOX is managed for the moment") 160 raise imap4.MailboxException("Only INBOX is managed for the moment")
161 self.name=name 161 self.mailbox=self.host.plugins["Maildir"].accessMessageBox(name,self.newMessage, profile)
162 self.mailbox=self.host.plugins["Maildir"].accessMessageBox("INBOX",self.newMessage)
163 162
164 def newMessage(self): 163 def newMessage(self):
165 """Called when a new message is in the mailbox""" 164 """Called when a new message is in the mailbox"""
166 debug ("newMessage signal received") 165 debug ("newMessage signal received")
167 nb_messages=self.getMessageCount() 166 nb_messages=self.getMessageCount()
205 204
206 def getUnseenCount(self): 205 def getUnseenCount(self):
207 """Return the number of messages with the 'Unseen' flag. 206 """Return the number of messages with the 'Unseen' flag.
208 """ 207 """
209 debug('getUnseenCount') 208 debug('getUnseenCount')
210 return self.getMessageCount()-len(self.mailbox.getMessageIdsWithFlag('\\Seen')) 209 return self.getMessageCount()-len(self.mailbox.getMessageIdsWithFlag('\\SEEN'))
211 210
212 def isWriteable(self): 211 def isWriteable(self):
213 """Get the read/write status of the mailbox. 212 """Get the read/write status of the mailbox.
214 @return: A true value if write permission is allowed, a false value otherwise. 213 @return: A true value if write permission is allowed, a false value otherwise.
215 """ 214 """
269 @return: A deferred whose callback is invoked with the message 268 @return: A deferred whose callback is invoked with the message
270 id if the message is added successfully and whose errback is 269 id if the message is added successfully and whose errback is
271 invoked otherwise. 270 invoked otherwise.
272 """ 271 """
273 debug('addMessage') 272 debug('addMessage')
274 raise NotImplementedError 273 raise imap4.MailboxException("Client message addition not implemented yet")
275 274
276 def expunge(self): 275 def expunge(self):
277 """Remove all messages flagged \\Deleted. 276 """Remove all messages flagged \\Deleted.
278 @return: The list of message sequence numbers which were deleted, 277 @return: The list of message sequence numbers which were deleted,
279 or a Deferred whose callback will be invoked with such a list. 278 or a Deferred whose callback will be invoked with such a list.
321 been performed, or a Deferred whose callback will be invoked with 320 been performed, or a Deferred whose callback will be invoked with
322 such a dict. 321 such a dict.
323 """ 322 """
324 debug('store') 323 debug('store')
325 324
325 flags=[flag.upper() for flag in flags]
326
326 def updateFlags(getF,setF): 327 def updateFlags(getF,setF):
327 ret = {} 328 ret = {}
328 for mess_id in messages: 329 for mess_id in messages:
329 if (uid and mess_id == None) or (not uid and mess_id>self.getMessageCount()): 330 if (uid and mess_id == None) or (not uid and mess_id>self.getMessageCount()):
330 break 331 break
333 _flags.difference_update(set(flags)) 334 _flags.difference_update(set(flags))
334 else: 335 else:
335 _flags.update(set(flags)) 336 _flags.update(set(flags))
336 new_flags=list(_flags) 337 new_flags=list(_flags)
337 setF(mess_id, new_flags) 338 setF(mess_id, new_flags)
338 ret[mess_id] = new_flags 339 ret[mess_id] = tuple(new_flags)
339 return ret 340 return ret
340 341
341 if uid: 342 if uid:
342 messages.last = self.mailbox.getMaxUid() 343 messages.last = self.mailbox.getMaxUid()
343 messages.getnext = self.mailbox.getNextExistingUid 344 messages.getnext = self.mailbox.getNextExistingUid
344 return updateFlags(self.mailbox.getFlagsUid,self.mailbox.setFlagsUid) 345 ret = updateFlags(self.mailbox.getFlagsUid,self.mailbox.setFlagsUid)
346 for listener in self.listeners:
347 listener.flagsChanged(ret)
348 return ret
349
345 else: 350 else:
346 messages.last = self.getMessageCount() 351 messages.last = self.getMessageCount()
347 return updateFlags(self.mailbox.getFlags,self.mailbox.setFlags) 352 ret = updateFlags(self.mailbox.getFlags,self.mailbox.setFlags)
353 newFlags={}
354 for idx in ret:
355 #we have to convert idx to uid for the listeners
356 newFlags[self.mailbox.getUid(idx)] = ret[idx]
357 for listener in self.listeners:
358 listener.flagsChanged(newFlags)
359 return ret
348 360
349 def getFlags(self): 361 def getFlags(self):
350 """Return the flags defined in this mailbox 362 """Return the flags defined in this mailbox
351 Flags with the \\ prefix are reserved for use as system flags. 363 Flags with the \\ prefix are reserved for use as system flags.
352 @return: A list of the flags that can be set on messages in this mailbox. 364 @return: A list of the flags that can be set on messages in this mailbox.
353 """ 365 """
354 debug('getFlags') 366 debug('getFlags')
355 return ['\Seen','\Answered','\Flagged','\Deleted','\Draft'] #TODO: add '\Recent' 367 return ['\\SEEN','\\ANSWERED','\\FLAGGED','\\DELETED','\\DRAFT'] #TODO: add '\\RECENT'
356 368
357 def getHierarchicalDelimiter(self): 369 def getHierarchicalDelimiter(self):
358 """Get the character which delimits namespaces for in this mailbox. 370 """Get the character which delimits namespaces for in this mailbox.
359 """ 371 """
360 debug('getHierarchicalDelimiter') 372 debug('getHierarchicalDelimiter')
361 return '.' 373 return '.'
362 374
363 375 class ImapSatAccount(imap4.MemoryAccount):
364
365 class ImapAccount(imap4.MemoryAccount):
366 #implements(imap4.IAccount) 376 #implements(imap4.IAccount)
367 # Actually implement the interface here 377
368 378 def __init__(self, host, profile):
369 def __init__(self, host, name):
370 debug("ImapAccount init") 379 debug("ImapAccount init")
371 self.host=host 380 self.host=host
372 imap4.MemoryAccount.__init__(self,name) 381 self.profile=profile
382 imap4.MemoryAccount.__init__(self,profile)
373 self.addMailbox("Inbox") #We only manage Inbox for the moment 383 self.addMailbox("Inbox") #We only manage Inbox for the moment
374 debug ('INBOX added') 384 debug ('INBOX added')
375 385
376 def _emptyMailbox(self, name, id): 386 def _emptyMailbox(self, name, id):
377 return SatMailbox(self.host,name) 387 return SatMailbox(self.host,name,self.profile)
378 388
379 389
380 class ImapRealm: 390 class ImapRealm:
381 implements(portal.IRealm) 391 implements(portal.IRealm)
382 392
383 def __init__(self,host): 393 def __init__(self,host):
384 self.host = host 394 self.host = host
385 395
386 def requestAvatar(self, avatarID, mind, *interfaces): 396 def requestAvatar(self, avatarID, mind, *interfaces):
387 debug('requestAvatar') 397 debug('requestAvatar')
398 profile=avatarID.decode('utf-8')
388 if imap4.IAccount not in interfaces: 399 if imap4.IAccount not in interfaces:
389 raise NotImplementedError 400 raise NotImplementedError
390 return imap4.IAccount, ImapAccount(self.host,avatarID), lambda:None 401 return imap4.IAccount, ImapSatAccount(self.host,profile), lambda:None
402
403 class SatProfileCredentialChecker:
404 """
405 This credential checker check against SàT's profile and associated jabber's password
406 Check if the profile exists, and if the password is OK
407 Return the profile as avatarId
408 """
409 implements(checkers.ICredentialsChecker)
410 credentialInterfaces = (credentials.IUsernamePassword,
411 credentials.IUsernameHashedPassword)
412
413
414 def __init__(self, host):
415 self.host = host
416
417 def _cbPasswordMatch(self, matched, profile):
418 if matched:
419 return profile.encode('utf-8')
420 else:
421 return failure.Failure(cred_error.UnauthorizedLogin())
422
423 def requestAvatarId(self, credentials):
424 profiles = self.host.memory.getProfilesList()
425 if not credentials.username in profiles:
426 return defer.fail(cred_error.UnauthorizedLogin())
427 password = self.host.memory.getParamA("Password", "Connection", profile_key=credentials.username)
428 return defer.maybeDeferred(
429 credentials.checkPassword,
430 password).addCallback(
431 self._cbPasswordMatch, credentials.username)
391 432
392 class ImapServerFactory(protocol.ServerFactory): 433 class ImapServerFactory(protocol.ServerFactory):
393 protocol = imap4.IMAP4Server 434 protocol = imap4.IMAP4Server
394 435
395 def __init__(self, host): 436 def __init__(self, host):
403 444
404 def buildProtocol(self, addr): 445 def buildProtocol(self, addr):
405 debug ("Building protocole") 446 debug ("Building protocole")
406 prot = protocol.ServerFactory.buildProtocol(self, addr) 447 prot = protocol.ServerFactory.buildProtocol(self, addr)
407 prot.portal = portal.Portal(ImapRealm(self.host)) 448 prot.portal = portal.Portal(ImapRealm(self.host))
408 prot.portal.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(goffi="toto")) 449 prot.portal.registerChecker(SatProfileCredentialChecker(self.host))
409 return prot 450 return prot