Mercurial > libervia-backend
diff sat/plugins/plugin_misc_imap.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_misc_imap.py@33c8c4973743 |
children | 56f94936df1e |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_misc_imap.py Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,447 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# SàT plugin for managing imap server +# Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.i18n import _ +from sat.core.constants import Const as C +from sat.core.log import getLogger +log = getLogger(__name__) +from twisted.internet import protocol, defer +from twisted.cred import portal, checkers, credentials +from twisted.cred import error as cred_error +from twisted.mail import imap4 +from twisted.python import failure +from email.parser import Parser +import os +from cStringIO import StringIO +from twisted.internet import reactor + +from zope.interface import implements + +PLUGIN_INFO = { + C.PI_NAME: "IMAP server Plugin", + C.PI_IMPORT_NAME: "IMAP", + C.PI_TYPE: "Misc", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: ["Maildir"], + C.PI_MAIN: "IMAP_server", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("""Create an Imap server that you can use to read your "normal" type messages""") +} + + +class IMAP_server(object): + #TODO: connect profile on mailbox request, once password is accepted + + params = """ + <params> + <general> + <category name="Mail Server"> + <param name="IMAP Port" value="10143" type="int" constraint="1;65535" /> + </category> + </general> + </params> + """ + + def __init__(self, host): + log.info(_("Plugin Imap Server initialization")) + self.host = host + + #parameters + host.memory.updateParams(self.params) + + port = int(self.host.memory.getParamA("IMAP Port", "Mail Server")) + log.info(_("Launching IMAP server on port %d") % port) + + self.server_factory = ImapServerFactory(self.host) + reactor.listenTCP(port, self.server_factory) + + +class Message(object): + implements(imap4.IMessage) + + def __init__(self, uid, flags, mess_fp): + log.debug('Message Init') + self.uid = uid + self.flags = flags + self.mess_fp = mess_fp + self.message = Parser().parse(mess_fp) + + def getUID(self): + """Retrieve the unique identifier associated with this message. + """ + log.debug('getUID (message)') + return self.uid + + def getFlags(self): + """Retrieve the flags associated with this message. + @return: The flags, represented as strings. + """ + log.debug('getFlags') + return self.flags + + def getInternalDate(self): + """Retrieve the date internally associated with this message. + @return: An RFC822-formatted date string. + """ + log.debug('getInternalDate') + return self.message['Date'] + + def getHeaders(self, negate, *names): + """Retrieve a group of message headers. + @param names: The names of the headers to retrieve or omit. + @param negate: If True, indicates that the headers listed in names + should be omitted from the return value, rather than included. + @return: A mapping of header field names to header field values + """ + log.debug(u'getHeaders %s - %s' % (negate, names)) + final_dict = {} + to_check = [name.lower() for name in names] + for header in self.message.keys(): + if (negate and not header.lower() in to_check) or \ + (not negate and header.lower() in to_check): + final_dict[header] = self.message[header] + return final_dict + + def getBodyFile(self): + """Retrieve a file object containing only the body of this message. + """ + log.debug('getBodyFile') + return StringIO(self.message.get_payload()) + + def getSize(self): + """Retrieve the total size, in octets, of this message. + """ + log.debug('getSize') + self.mess_fp.seek(0, os.SEEK_END) + return self.mess_fp.tell() + + def isMultipart(self): + """Indicate whether this message has subparts. + """ + log.debug('isMultipart') + return False + + def getSubPart(self, part): + """Retrieve a MIME sub-message + @param part: The number of the part to retrieve, indexed from 0. + @return: The specified sub-part. + """ + log.debug('getSubPart') + return TypeError + + +class SatMailbox(object): + implements(imap4.IMailbox) + + def __init__(self, host, name, profile): + self.host = host + self.listeners = set() + log.debug(u'Mailbox init (%s)' % name) + if name != "INBOX": + raise imap4.MailboxException("Only INBOX is managed for the moment") + self.mailbox = self.host.plugins["Maildir"].accessMessageBox(name, self.messageNew, profile) + + def messageNew(self): + """Called when a new message is in the mailbox""" + log.debug("messageNew signal received") + nb_messages = self.getMessageCount() + for listener in self.listeners: + listener.newMessages(nb_messages, None) + + def getUIDValidity(self): + """Return the unique validity identifier for this mailbox. + """ + log.debug('getUIDValidity') + return 0 + + def getUIDNext(self): + """Return the likely UID for the next message added to this mailbox. + """ + log.debug('getUIDNext') + return self.mailbox.getNextUid() + + def getUID(self, message): + """Return the UID of a message in the mailbox + @param message: The message sequence number + @return: The UID of the message. + """ + log.debug(u'getUID (%i)' % message) + #return self.mailbox.getUid(message-1) #XXX: it seems that this method get uid and not message sequence number + return message + + def getMessageCount(self): + """Return the number of messages in this mailbox. + """ + log.debug('getMessageCount') + ret = self.mailbox.getMessageCount() + log.debug("count = %i" % ret) + return ret + + def getRecentCount(self): + """Return the number of messages with the 'Recent' flag. + """ + log.debug('getRecentCount') + return len(self.mailbox.getMessageIdsWithFlag('\\Recent')) + + def getUnseenCount(self): + """Return the number of messages with the 'Unseen' flag. + """ + log.debug('getUnseenCount') + return self.getMessageCount() - len(self.mailbox.getMessageIdsWithFlag('\\SEEN')) + + def isWriteable(self): + """Get the read/write status of the mailbox. + @return: A true value if write permission is allowed, a false value otherwise. + """ + log.debug('isWriteable') + return True + + def destroy(self): + """Called before this mailbox is deleted, permanently. + """ + log.debug('destroy') + + def requestStatus(self, names): + """Return status information about this mailbox. + @param names: The status names to return information regarding. + The possible values for each name are: MESSAGES, RECENT, UIDNEXT, + UIDVALIDITY, UNSEEN. + @return: A dictionary containing status information about the + requested names is returned. If the process of looking this + information up would be costly, a deferred whose callback will + eventually be passed this dictionary is returned instead. + """ + log.debug('requestStatus') + return imap4.statusRequestHelper(self, names) + + def addListener(self, listener): + """Add a mailbox change listener + + @type listener: Any object which implements C{IMailboxListener} + @param listener: An object to add to the set of those which will + be notified when the contents of this mailbox change. + """ + log.debug(u'addListener %s' % listener) + self.listeners.add(listener) + + def removeListener(self, listener): + """Remove a mailbox change listener + + @type listener: Any object previously added to and not removed from + this mailbox as a listener. + @param listener: The object to remove from the set of listeners. + + @raise ValueError: Raised when the given object is not a listener for + this mailbox. + """ + log.debug('removeListener') + if listener in self.listeners: + self.listeners.remove(listener) + else: + raise imap4.MailboxException('Trying to remove an unknown listener') + + def addMessage(self, message, flags=(), date=None): + """Add the given message to this mailbox. + @param message: The RFC822 formatted message + @param flags: The flags to associate with this message + @param date: If specified, the date to associate with this + @return: A deferred whose callback is invoked with the message + id if the message is added successfully and whose errback is + invoked otherwise. + """ + log.debug('addMessage') + raise imap4.MailboxException("Client message addition not implemented yet") + + def expunge(self): + """Remove all messages flagged \\Deleted. + @return: The list of message sequence numbers which were deleted, + or a Deferred whose callback will be invoked with such a list. + """ + log.debug('expunge') + self.mailbox.removeDeleted() + + def fetch(self, messages, uid): + """Retrieve one or more messages. + @param messages: The identifiers of messages to retrieve information + about + @param uid: If true, the IDs specified in the query are UIDs; + """ + log.debug(u'fetch (%s, %s)' % (messages, uid)) + if uid: + messages.last = self.mailbox.getMaxUid() + messages.getnext = self.mailbox.getNextExistingUid + for mess_uid in messages: + if mess_uid is None: + log.debug('stopping iteration') + raise StopIteration + try: + yield (mess_uid, Message(mess_uid, self.mailbox.getFlagsUid(mess_uid), self.mailbox.getMessageUid(mess_uid))) + except IndexError: + continue + else: + messages.last = self.getMessageCount() + for mess_idx in messages: + if mess_idx > self.getMessageCount(): + raise StopIteration + yield (mess_idx, Message(mess_idx, self.mailbox.getFlags(mess_idx), self.mailbox.getMessage(mess_idx - 1))) + + def store(self, messages, flags, mode, uid): + """Set the flags of one or more messages. + @param messages: The identifiers of the messages to set the flags of. + @param flags: The flags to set, unset, or add. + @param mode: If mode is -1, these flags should be removed from the + specified messages. If mode is 1, these flags should be added to + the specified messages. If mode is 0, all existing flags should be + cleared and these flags should be added. + @param uid: If true, the IDs specified in the query are UIDs; + otherwise they are message sequence IDs. + @return: A dict mapping message sequence numbers to sequences of str + representing the flags set on the message after this operation has + been performed, or a Deferred whose callback will be invoked with + such a dict. + """ + log.debug('store') + + flags = [flag.upper() for flag in flags] + + def updateFlags(getF, setF): + ret = {} + for mess_id in messages: + if (uid and mess_id is None) or (not uid and mess_id > self.getMessageCount()): + break + _flags = set(getF(mess_id) if mode else []) + if mode == -1: + _flags.difference_update(set(flags)) + else: + _flags.update(set(flags)) + new_flags = list(_flags) + setF(mess_id, new_flags) + ret[mess_id] = tuple(new_flags) + return ret + + if uid: + messages.last = self.mailbox.getMaxUid() + messages.getnext = self.mailbox.getNextExistingUid + ret = updateFlags(self.mailbox.getFlagsUid, self.mailbox.setFlagsUid) + for listener in self.listeners: + listener.flagsChanged(ret) + return ret + + else: + messages.last = self.getMessageCount() + ret = updateFlags(self.mailbox.getFlags, self.mailbox.setFlags) + newFlags = {} + for idx in ret: + #we have to convert idx to uid for the listeners + newFlags[self.mailbox.getUid(idx)] = ret[idx] + for listener in self.listeners: + listener.flagsChanged(newFlags) + return ret + + def getFlags(self): + """Return the flags defined in this mailbox + Flags with the \\ prefix are reserved for use as system flags. + @return: A list of the flags that can be set on messages in this mailbox. + """ + log.debug('getFlags') + return ['\\SEEN', '\\ANSWERED', '\\FLAGGED', '\\DELETED', '\\DRAFT'] # TODO: add '\\RECENT' + + def getHierarchicalDelimiter(self): + """Get the character which delimits namespaces for in this mailbox. + """ + log.debug('getHierarchicalDelimiter') + return '.' + + +class ImapSatAccount(imap4.MemoryAccount): + #implements(imap4.IAccount) + + def __init__(self, host, profile): + log.debug("ImapAccount init") + self.host = host + self.profile = profile + imap4.MemoryAccount.__init__(self, profile) + self.addMailbox("Inbox") # We only manage Inbox for the moment + log.debug('INBOX added') + + def _emptyMailbox(self, name, id): + return SatMailbox(self.host, name, self.profile) + + +class ImapRealm(object): + implements(portal.IRealm) + + def __init__(self, host): + self.host = host + + def requestAvatar(self, avatarID, mind, *interfaces): + log.debug('requestAvatar') + profile = avatarID.decode('utf-8') + if imap4.IAccount not in interfaces: + raise NotImplementedError + return imap4.IAccount, ImapSatAccount(self.host, profile), lambda: None + + +class SatProfileCredentialChecker(object): + """ + This credential checker check against SàT's profile and associated jabber's password + Check if the profile exists, and if the password is OK + Return the profile as avatarId + """ + implements(checkers.ICredentialsChecker) + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IUsernameHashedPassword) + + def __init__(self, host): + self.host = host + + def _cbPasswordMatch(self, matched, profile): + if matched: + return profile.encode('utf-8') + else: + return failure.Failure(cred_error.UnauthorizedLogin()) + + def requestAvatarId(self, credentials): + profiles = self.host.memory.getProfilesList() + if not credentials.username in profiles: + return defer.fail(cred_error.UnauthorizedLogin()) + d = self.host.memory.asyncGetParamA("Password", "Connection", profile_key=credentials.username) + d.addCallback(lambda password: credentials.checkPassword(password)) + d.addCallback(self._cbPasswordMatch, credentials.username) + return d + + +class ImapServerFactory(protocol.ServerFactory): + protocol = imap4.IMAP4Server + + def __init__(self, host): + self.host = host + + def startedConnecting(self, connector): + log.debug(_("IMAP server connection started")) + + def clientConnectionLost(self, connector, reason): + log.debug(_(u"IMAP server connection lost (reason: %s)"), reason) + + def buildProtocol(self, addr): + log.debug("Building protocol") + prot = protocol.ServerFactory.buildProtocol(self, addr) + prot.portal = portal.Portal(ImapRealm(self.host)) + prot.portal.registerChecker(SatProfileCredentialChecker(self.host)) + return prot