# HG changeset patch # User Goffi # Date 1575640922 -3600 # Node ID fd593b448bee1b4070b3ccf459e0bc8e93d00d22 # Parent 16925f494820390c24fca8a48153b95472bed9de plugins (imap, maildir, smtp): removed plugins IMAP, Maildir and SMTP: they were experimental plugins, are not used and Maildir has not been ported to Python 3 diff -r 16925f494820 -r fd593b448bee CHANGELOG --- a/CHANGELOG Thu Dec 05 23:05:16 2019 +0100 +++ b/CHANGELOG Fri Dec 06 15:02:02 2019 +0100 @@ -2,6 +2,8 @@ v 0.8.0 « La Cecília » (NOT RELEASED YET): - Python 3 port + - removed plugins IMAP, Maildir and SMTP: they were experimental plugins, are not used + and Maildir has not been ported to Python 3 - Cagou: - new "share" widget - Cagou (Android): diff -r 16925f494820 -r fd593b448bee sat/plugins/plugin_misc_imap.py --- a/sat/plugins/plugin_misc_imap.py Thu Dec 05 23:05:16 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,480 +0,0 @@ -#!/usr/bin/env python3 -# -*- 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 . - -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 io import StringIO -from twisted.internet import reactor - -from zope.interface import implementer - -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 = """ - - - - - - - - """ - - 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) - - -@implementer(imap4.IMessage) -class Message(object): - - 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("getHeaders %s - %s" % (negate, names)) - final_dict = {} - to_check = [name.lower() for name in names] - for header in list(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 - - -@implementer(imap4.IMailbox) -class SatMailbox(object): - - def __init__(self, host, name, profile): - self.host = host - self.listeners = set() - log.debug("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("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("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("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) - - -@implementer(portal.IRealm) -class ImapRealm(object): - - def __init__(self, host): - self.host = host - - def requestAvatar(self, avatarID, mind, *interfaces): - log.debug("requestAvatar") - profile = avatarID - if imap4.IAccount not in interfaces: - raise NotImplementedError - return imap4.IAccount, ImapSatAccount(self.host, profile), lambda: None - - -@implementer(checkers.ICredentialsChecker) -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 - """ - - 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(_("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 diff -r 16925f494820 -r fd593b448bee sat/plugins/plugin_misc_maildir.py --- a/sat/plugins/plugin_misc_maildir.py Thu Dec 05 23:05:16 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,500 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# SàT plugin for managing Maildir type mail boxes -# 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 . - -from sat.core.i18n import D_, _ -from sat.core.constants import Const as C -from sat.core.log import getLogger -log = getLogger(__name__) -import warnings -warnings.filterwarnings('ignore', 'the MimeWriter', DeprecationWarning, 'twisted') # FIXME: to be removed, see http://twistedmatrix.com/trac/ticket/4038 -from twisted.mail import maildir -import email.message -import email.utils -import os -from sat.core.exceptions import ProfileUnknownError -from sat.memory.persistent import PersistentBinaryDict - - -PLUGIN_INFO = { - C.PI_NAME: "Maildir Plugin", - C.PI_IMPORT_NAME: "Maildir", - C.PI_TYPE: "Misc", - C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: [], - C.PI_MAIN: "MaildirBox", - C.PI_HANDLER: "no", - C.PI_DESCRIPTION: _("""Intercept "normal" type messages, and put them in a Maildir type box""") -} - -MAILDIR_PATH = "Maildir" -CATEGORY = D_("Mail Server") -NAME = D_('Block "normal" messages propagation') -# FIXME: (very) old and (very) experimental code, need a big cleaning/review or to be deprecated - - -class MaildirError(Exception): - pass - - -class MaildirBox(object): - params = """ - - - - - - - - """.format(category_name=CATEGORY, - category_label=_(CATEGORY), - name=NAME, - label=_(NAME), - ) - - def __init__(self, host): - log.info(_("Plugin Maildir initialization")) - self.host = host - host.memory.updateParams(self.params) - - self.__observed = {} - self.data = {} # list of profile spectific data. key = profile, value = PersistentBinaryDict where key=mailbox name, - # and value is a dictionnary with the following value - # - cur_idx: value of the current unique integer increment (UID) - # - message_id (as returned by MaildirMailbox): a tuple of (UID, [flag1, flag2, ...]) - self.__mailboxes = {} # key: profile, value: {boxname: MailboxUser instance} - - #the triggers - host.trigger.add("MessageReceived", self.messageReceivedTrigger) - - def profileConnected(self, client): - """Called on client connection, create profile data""" - profile = client.profile - self.data[profile] = PersistentBinaryDict("plugin_maildir", profile) - self.__mailboxes[profile] = {} - - def dataLoaded(ignore): - if not self.data[profile]: - #the mailbox is new, we initiate the data - self.data[profile]["INBOX"] = {"cur_idx": 0} - self.data[profile].load().addCallback(dataLoaded) - - def profileDisconnected(self, client): - """Called on profile disconnection, free profile's resources""" - profile = client.profile - del self.__mailboxes[profile] - del self.data[profile] - - def messageReceivedTrigger(self, client, message, post_treat): - """This trigger catch normal message and put the in the Maildir box. - If the message is not of "normal" type, do nothing - @param message: message xmlstrem - @return: False if it's a normal message, True else""" - profile = client.profile - for e in message.elements(C.NS_CLIENT, 'body'): - mess_type = message.getAttribute('type', 'normal') - if mess_type != 'normal': - return True - self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) - return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile) - return True - - def accessMessageBox(self, boxname, observer=None, profile_key=C.PROF_KEY_NONE): - """Create and return a MailboxUser instance - @param boxname: name of the box - @param observer: method to call when a NewMessage arrive""" - profile = self.host.memory.getProfileName(profile_key) - if not profile: - raise ProfileUnknownError(profile_key) - if boxname not in self.__mailboxes[profile]: - self.__mailboxes[profile][boxname] = MailboxUser(self, boxname, observer, profile=profile) - else: - if observer: - self.addObserver(observer, profile, boxname) - return self.__mailboxes[profile][boxname] - - def _getProfilePath(self, profile): - """Return a unique path for profile's mailbox - The path must be unique, usable as a dir name, and bijectional""" - return profile.replace('/', '_').replace('..', '_') # FIXME: this is too naive to work well, must be improved - - def _removeBoxAccess(self, boxname, mailboxUser, profile): - """Remove a reference to a box - @param name: name of the box - @param mailboxUser: MailboxUser instance""" - if boxname not in self.__mailboxes: - err_msg = _("Trying to remove an mailboxUser not referenced") - log.error(_("INTERNAL ERROR: ") + err_msg) - raise MaildirError(err_msg) - assert self.__mailboxes[profile][boxname] == mailboxUser - del self.__mailboxes[profile][boxname] - - def _checkBoxReference(self, boxname, profile): - """Check if there is a reference on a box, and return it - @param boxname: name of the box to check - @return: MailboxUser instance or None""" - if profile in self.__mailboxes: - if boxname in self.__mailboxes[profile]: - return self.__mailboxes[profile][boxname] - - def __getBoxData(self, boxname, profile): - """Return the date of a box""" - try: - return self.data[profile][boxname] # the boxname MUST exist in the data - except KeyError: - err_msg = _("Boxname doesn't exist in internal data") - log.error(_("INTERNAL ERROR: ") + err_msg) - raise MaildirError(err_msg) - - def getUid(self, boxname, message_id, profile): - """Return an unique integer, always ascending, for a message - This is mainly needed for the IMAP protocol - @param boxname: name of the box where the message is - @param message_id: unique id of the message as given by MaildirMailbox - @return: Integer UID""" - box_data = self.__getBoxData(boxname, profile) - if message_id in box_data: - ret = box_data[message_id][0] - else: - box_data['cur_idx'] += 1 - box_data[message_id] = [box_data['cur_idx'], []] - ret = box_data[message_id] - self.data[profile].force(boxname) - return ret - - def getNextUid(self, boxname, profile): - """Return next unique integer that will generated - This is mainly needed for the IMAP protocol - @param boxname: name of the box where the message is - @return: Integer UID""" - box_data = self.__getBoxData(boxname, profile) - return box_data['cur_idx'] + 1 - - def getNextExistingUid(self, boxname, uid, profile): - """Give the next uid of existing message - @param boxname: name of the box where the message is - @param uid: uid to start from - @return: uid or None if the is no more message""" - box_data = self.__getBoxData(boxname, profile) - idx = uid + 1 - while self.getIdFromUid(boxname, idx, profile) is None: # TODO: this is highly inefficient because getIdfromUid is inefficient, fix this - idx += 1 - if idx > box_data['cur_idx']: - return None - return idx - - def getMaxUid(self, boxname, profile): - """Give the max existing uid - @param boxname: name of the box where the message is - @return: uid""" - box_data = self.__getBoxData(boxname, profile) - return box_data['cur_idx'] - - def getIdFromUid(self, boxname, message_uid, profile): - """Return the message unique id from it's integer UID - @param boxname: name of the box where the message is - @param message_uid: unique integer identifier - @return: unique id of the message as given by MaildirMailbox or None if not found""" - box_data = self.__getBoxData(boxname, profile) - for message_id in list(box_data.keys()): # TODO: this is highly inefficient on big mailbox, must be replaced in the future - if message_id == 'cur_idx': - continue - if box_data[message_id][0] == message_uid: - return message_id - return None - - def getFlags(self, boxname, mess_id, profile): - """Return the messages flags - @param boxname: name of the box where the message is - @param message_idx: message id as given by MaildirMailbox - @return: list of strings""" - box_data = self.__getBoxData(boxname, profile) - if mess_id not in box_data: - raise MaildirError("Trying to get flags from an unexisting message") - return box_data[mess_id][1] - - def setFlags(self, boxname, mess_id, flags, profile): - """Change the flags of the message - @param boxname: name of the box where the message is - @param message_idx: message id as given by MaildirMailbox - @param flags: list of strings - """ - box_data = self.__getBoxData(boxname, profile) - assert(type(flags) == list) - flags = [flag.upper() for flag in flags] # we store every flag UPPERCASE - if mess_id not in box_data: - raise MaildirError("Trying to set flags for an unexisting message") - box_data[mess_id][1] = flags - self.data[profile].force(boxname) - - def getMessageIdsWithFlag(self, boxname, flag, profile): - """Return ids of messages where a flag is set - @param boxname: name of the box where the message is - @param flag: flag to check - @return: list of id (as given by MaildirMailbox)""" - box_data = self.__getBoxData(boxname, profile) - assert(isinstance(flag, str)) - flag = flag.upper() - result = [] - for key in box_data: - if key == 'cur_idx': - continue - if flag in box_data[key][1]: - result.append(key) - return result - - def purgeDeleted(self, boxname, profile): - """Remove data for messages with flag "\\Deleted" - @param boxname: name of the box where the message is - """ - box_data = self.__getBoxData(boxname, profile) - for mess_id in self.getMessageIdsWithFlag(boxname, "\\Deleted", profile): - del(box_data[mess_id]) - self.data[profile].force(boxname) - - def cleanTable(self, boxname, existant_id, profile): - """Remove mails which no longuer exist from the table - @param boxname: name of the box to clean - @param existant_id: list of id which actually exist""" - box_data = self.__getBoxData(boxname, profile) - to_remove = [] - for key in box_data: - if key not in existant_id and key != "cur_idx": - to_remove.append(key) - for key in to_remove: - del box_data[key] - - def addObserver(self, callback, profile, boxname, signal="NEW_MESSAGE"): - """Add an observer for maildir box changes - @param callback: method to call when the the box is updated - @param boxname: name of the box to observe - @param signal: which signal is observed by the caller""" - if (profile, boxname) not in self.__observed: - self.__observed[(profile, boxname)] = {} - if signal not in self.__observed[(profile, boxname)]: - self.__observed[(profile, boxname)][signal] = set() - self.__observed[(profile, boxname)][signal].add(callback) - - def removeObserver(self, callback, profile, boxname, signal="NEW_MESSAGE"): - """Remove an observer of maildir box changes - @param callback: method to remove from obervers - @param boxname: name of the box which was observed - @param signal: which signal was observed by the caller""" - if (profile, boxname) not in self.__observed: - err_msg = _("Trying to remove an observer for an inexistant mailbox") - log.error(_("INTERNAL ERROR: ") + err_msg) - raise MaildirError(err_msg) - if signal not in self.__observed[(profile, boxname)]: - err_msg = _("Trying to remove an inexistant observer, no observer for this signal") - log.error(_("INTERNAL ERROR: ") + err_msg) - raise MaildirError(err_msg) - if not callback in self.__observed[(profile, boxname)][signal]: - err_msg = _("Trying to remove an inexistant observer") - log.error(_("INTERNAL ERROR: ") + err_msg) - raise MaildirError(err_msg) - self.__observed[(profile, boxname)][signal].remove(callback) - - def emitSignal(self, profile, boxname, signal_name): - """Emit the signal to observer""" - log.debug('emitSignal %s %s %s' % (profile, boxname, signal_name)) - try: - for observer_cb in self.__observed[(profile, boxname)][signal_name]: - observer_cb() - except KeyError: - pass - - -class MailboxUser(object): - """This class is used to access a mailbox""" - - def xmppMessage2mail(self, message): - """Convert the XMPP's XML message to a basic rfc2822 message - @param xml: domish.Element of the message - @return: string email""" - mail = email.message.Message() - mail['MIME-Version'] = "1.0" - mail['Content-Type'] = "text/plain; charset=UTF-8; format=flowed" - mail['Content-Transfer-Encoding'] = "8bit" - mail['From'] = message['from'].encode('utf-8') - mail['To'] = message['to'].encode('utf-8') - mail['Date'] = email.utils.formatdate().encode('utf-8') - #TODO: save thread id - for e in message.elements(): - if e.name == "body": - mail.set_payload(e.children[0].encode('utf-8')) - elif e.name == "subject": - mail['Subject'] = e.children[0].encode('utf-8') - return mail.as_string() - - def __init__(self, _maildir, name, observer=None, profile=C.PROF_KEY_NONE): - """@param _maildir: the main MaildirBox instance - @param name: name of the mailbox - @param profile: real profile (ie not a profile_key) - THIS OBJECT MUST NOT BE USED DIRECTLY: use MaildirBox.accessMessageBox instead""" - if _maildir._checkBoxReference(name, profile): - log.error("INTERNAL ERROR: MailboxUser MUST NOT be instancied directly") - raise MaildirError('double MailboxUser instanciation') - if name != "INBOX": - raise NotImplementedError - self.name = name - self.profile = profile - self.maildir = _maildir - profile_path = self.maildir._getProfilePath(profile) - full_profile_path = os.path.join(self.maildir.host.memory.getConfig('', 'local_dir'), 'maildir', profile_path) - if not os.path.exists(full_profile_path): - os.makedirs(full_profile_path, 0o700) - mailbox_path = os.path.join(full_profile_path, MAILDIR_PATH) - self.mailbox_path = mailbox_path - self.mailbox = maildir.MaildirMailbox(mailbox_path) - self.observer = observer - self.__uid_table_update() - - if observer: - log.debug("adding observer for %s (%s)" % (name, profile)) - self.maildir.addObserver(observer, profile, name, "NEW_MESSAGE") - - def __uid_table_update(self): - existant_id = [] - for mess_idx in range(self.getMessageCount()): - #we update the uid table - existant_id.append(self.getId(mess_idx)) - self.getUid(mess_idx) - self.maildir.cleanTable(self.name, existant_id, profile=self.profile) - - def __del__(self): - if self.observer: - log.debug("removing observer for %s" % self.name) - self._maildir.removeObserver(self.observer, self.name, "NEW_MESSAGE") - self.maildir._removeBoxAccess(self.name, self, profile=self.profile) - - def addMessage(self, message): - """Add a message to the box - @param message: XMPP XML message""" - self.mailbox.appendMessage(self.xmppMessage2mail(message)).addCallback(self.emitSignal, "NEW_MESSAGE") - - def emitSignal(self, ignore, signal): - """Emit the signal to the observers""" - if signal == "NEW_MESSAGE": - self.getUid(self.getMessageCount() - 1) # XXX: we make an uid for the last message added - self.maildir.emitSignal(self.profile, self.name, signal) - - def getId(self, mess_idx): - """Return the Unique ID of the message - @mess_idx: message index""" - return self.mailbox.getUidl(mess_idx) - - def getUid(self, mess_idx): - """Return a unique interger id for the message, always ascending""" - mess_id = self.getId(mess_idx) - return self.maildir.getUid(self.name, mess_id, profile=self.profile) - - def getNextUid(self): - return self.maildir.getNextUid(self.name, profile=self.profile) - - def getNextExistingUid(self, uid): - return self.maildir.getNextExistingUid(self.name, uid, profile=self.profile) - - def getMaxUid(self): - return self.maildir.getMaxUid(self.name, profile=self.profile) - - def getMessageCount(self): - """Return number of mails present in this box""" - return len(self.mailbox.list) - - def getMessageIdx(self, mess_idx): - """Return the full message - @mess_idx: message index""" - return self.mailbox.getMessage(mess_idx) - - def getIdxFromUid(self, mess_uid): - """Return the message index from the uid - @param mess_uid: message unique identifier - @return: message index, as managed by MaildirMailbox""" - for mess_idx in range(self.getMessageCount()): - if self.getUid(mess_idx) == mess_uid: - return mess_idx - raise IndexError - - def getIdxFromId(self, mess_id): - """Return the message index from the unique index - @param mess_id: message unique index as given by MaildirMailbox - @return: message sequence index""" - for mess_idx in range(self.getMessageCount()): - if self.mailbox.getUidl(mess_idx) == mess_id: - return mess_idx - raise IndexError - - def getMessage(self, mess_idx): - """Return the full message - @param mess_idx: message index""" - return self.mailbox.getMessage(mess_idx) - - def getMessageUid(self, mess_uid): - """Return the full message - @param mess_idx: message unique identifier""" - return self.mailbox.getMessage(self.getIdxFromUid(mess_uid)) - - def getFlags(self, mess_idx): - """Return the flags of the message - @param mess_idx: message index - @return: list of strings""" - id = self.getId(mess_idx) - return self.maildir.getFlags(self.name, id, profile=self.profile) - - def getFlagsUid(self, mess_uid): - """Return the flags of the message - @param mess_uid: message unique identifier - @return: list of strings""" - id = self.maildir.getIdFromUid(self.name, mess_uid, profile=self.profile) - return self.maildir.getFlags(self.name, id, profile=self.profile) - - def setFlags(self, mess_idx, flags): - """Change the flags of the message - @param mess_idx: message index - @param flags: list of strings - """ - id = self.getId(mess_idx) - self.maildir.setFlags(self.name, id, flags, profile=self.profile) - - def setFlagsUid(self, mess_uid, flags): - """Change the flags of the message - @param mess_uid: message unique identifier - @param flags: list of strings - """ - id = self.maildir.getIdFromUid(self.name, mess_uid, profile=self.profile) - return self.maildir.setFlags(self.name, id, flags, profile=self.profile) - - def getMessageIdsWithFlag(self, flag): - """Return ids of messages where a flag is set - @param flag: flag to check - @return: list of id (as given by MaildirMailbox)""" - return self.maildir.getMessageIdsWithFlag(self.name, flag, profile=self.profile) - - def removeDeleted(self): - """Actually delete message flagged "\\Deleted" - Also purge the internal data of these messages - """ - for mess_id in self.getMessageIdsWithFlag("\\Deleted"): - print(("Deleting %s" % mess_id)) - self.mailbox.deleteMessage(self.getIdxFromId(mess_id)) - self.mailbox = maildir.MaildirMailbox(self.mailbox_path) # We need to reparse the dir to have coherent indexing - self.maildir.purgeDeleted(self.name, profile=self.profile) - - def emptyTrash(self): - """Delete everything in the .Trash dir""" - pass #TODO diff -r 16925f494820 -r fd593b448bee sat/plugins/plugin_misc_smtp.py --- a/sat/plugins/plugin_misc_smtp.py Thu Dec 05 23:05:16 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# SàT plugin for managing smtp 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 . - -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 defer -from twisted.cred import portal, checkers, credentials -from twisted.cred import error as cred_error -from twisted.mail import smtp -from twisted.python import failure -from email.parser import Parser -from email.utils import parseaddr -from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials -from twisted.internet import reactor -import sys - -from zope.interface import implementer - -PLUGIN_INFO = { - C.PI_NAME: "SMTP server Plugin", - C.PI_IMPORT_NAME: "SMTP", - C.PI_TYPE: "Misc", - C.PI_PROTOCOLS: [], - C.PI_DEPENDENCIES: ["Maildir"], - C.PI_MAIN: "SMTP_server", - C.PI_HANDLER: "no", - C.PI_DESCRIPTION: _( - """Create a SMTP server that you can use to send your "normal" type messages""" - ), -} - - -class SMTP_server(object): - - params = """ - - - - - - - - """ - - def __init__(self, host): - log.info(_("Plugin SMTP Server initialization")) - self.host = host - - # parameters - host.memory.updateParams(self.params) - - port = int(self.host.memory.getParamA("SMTP Port", "Mail Server")) - log.info(_("Launching SMTP server on port %d") % port) - - self.server_factory = SmtpServerFactory(self.host) - reactor.listenTCP(port, self.server_factory) - - -@implementer(smtp.IMessage) -class SatSmtpMessage(object): - - def __init__(self, host, profile): - self.host = host - self.profile = profile - self.message = [] - - def lineReceived(self, line): - """handle another line""" - self.message.append(line) - - def eomReceived(self): - """handle end of message""" - mail = Parser().parsestr("\n".join(self.message)) - try: - self.host._sendMessage( - parseaddr(mail["to"].decode("utf-8", "replace"))[1], - mail.get_payload().decode( - "utf-8", "replace" - ), # TODO: manage other charsets - subject=mail["subject"].decode("utf-8", "replace"), - mess_type="normal", - profile_key=self.profile, - ) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - log.error( - _("Can't send message: %s") % exc_value - ) # The email is invalid or incorreclty parsed - return defer.fail() - self.message = None - return defer.succeed(None) - - def connectionLost(self): - """handle message truncated""" - raise smtp.SMTPError - - -@implementer(smtp.IMessageDelivery) -class SatSmtpDelivery(object): - - def __init__(self, host, profile): - self.host = host - self.profile = profile - - def receivedHeader(self, helo, origin, recipients): - """ - Generate the Received header for a message - @param helo: The argument to the HELO command and the client's IP - address. - @param origin: The address the message is from - @param recipients: A list of the addresses for which this message - is bound. - @return: The full \"Received\" header string. - """ - return "Received:" - - def validateTo(self, user): - """ - Validate the address for which the message is destined. - @param user: The address to validate. - @return: A Deferred which becomes, or a callable which - takes no arguments and returns an object implementing IMessage. - This will be called and the returned object used to deliver the - message when it arrives. - """ - return lambda: SatSmtpMessage(self.host, self.profile) - - def validateFrom(self, helo, origin): - """ - Validate the address from which the message originates. - @param helo: The argument to the HELO command and the client's IP - address. - @param origin: The address the message is from - @return: origin or a Deferred whose callback will be - passed origin. - """ - return origin - - -@implementer(portal.IRealm) -class SmtpRealm(object): - - def __init__(self, host): - self.host = host - - def requestAvatar(self, avatarID, mind, *interfaces): - log.debug("requestAvatar") - profile = avatarID - if smtp.IMessageDelivery not in interfaces: - raise NotImplementedError - return smtp.IMessageDelivery, SatSmtpDelivery(self.host, profile), lambda: None - - -@implementer(checkers.ICredentialsChecker) -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 - """ - - 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(credentials.checkPassword) - d.addCallback(self._cbPasswordMatch, credentials.username) - return d - - -class SmtpServerFactory(smtp.SMTPFactory): - def __init__(self, host): - self.protocol = smtp.ESMTP - self.host = host - _portal = portal.Portal(SmtpRealm(self.host)) - _portal.registerChecker(SatProfileCredentialChecker(self.host)) - smtp.SMTPFactory.__init__(self, _portal) - - def startedConnecting(self, connector): - log.debug(_("SMTP server connection started")) - smtp.SMTPFactory.startedConnecting(self, connector) - - def clientConnectionLost(self, connector, reason): - log.debug(_("SMTP server connection lost (reason: %s)"), reason) - smtp.SMTPFactory.clientConnectionLost(self, connector, reason) - - def buildProtocol(self, addr): - p = smtp.SMTPFactory.buildProtocol(self, addr) - # add the challengers from imap4, more secure and complicated challengers are available - p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials} - return p