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
This commit is contained in:
Goffi 2019-12-06 15:02:02 +01:00
commit 808790933a
4 changed files with 2 additions and 1207 deletions

View file

@ -2,6 +2,8 @@ All theses changelogs are not exhaustive, please check the Mercurial repository
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):

View file

@ -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 <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 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 = """
<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)
@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

View file

@ -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 <http://www.gnu.org/licenses/>.
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 = """
<params>
<individual>
<category name='{category_name}' label='{category_label}'>
<param name='{name}' label='{label}' value="false" type="bool" security="4" />
</category>
</individual>
</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

View file

@ -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 <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 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 = """
<params>
<general>
<category name="Mail Server">
<param name="SMTP Port" value="10125" type="int" constraint="1;65535" />
</category>
</general>
</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