Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
inpos | ea035c1e54 | |
inpos | e60d89c1b6 | |
inpos | 5e23c30797 | |
inpos | 3b43af2b2f | |
inpos | 9956b94649 | |
inpos | 4547c85237 |
90
main.py
90
main.py
|
@ -1,31 +1,59 @@
|
||||||
import asyncio
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
clients = {} # task -> (reader, writer)
|
from twisted.cred.checkers import ICredentialsChecker
|
||||||
|
from twisted.cred import credentials, error
|
||||||
def client_connected_handler(client_reader, client_writer):
|
from twisted.python import failure
|
||||||
# Start a new asyncio.Task to handle this specific client connection
|
from twisted.internet import reactor, defer
|
||||||
task = asyncio.Task(handle_client(client_reader, client_writer))
|
from twisted.internet.task import LoopingCall
|
||||||
clients[task] = (client_reader, client_writer)
|
|
||||||
|
from zope.interface import implements
|
||||||
def client_done(task):
|
|
||||||
# When the tasks that handles the specific client connection is done
|
from mech_smtp import SerpentSMTPFactory, smtp_portal
|
||||||
del clients[task]
|
from mech_imap import SerpentIMAPFactory, imap_portal
|
||||||
|
from serpent.usrpwd import dbs
|
||||||
# Add the client_done callback to be run when the future becomes done
|
from serpent.queue import squeue
|
||||||
task.add_done_callback(client_done)
|
from serpent.config import conf
|
||||||
|
|
||||||
@asyncio.coroutine
|
class CredChecker(object):
|
||||||
def handle_client(client_reader, client_writer):
|
'''Класс проверки данных авторизации.
|
||||||
# Handle the requests for a specific client with a line oriented protocol
|
Параметром в конструктор передаётся список (list()) объектов баз пользователей.'''
|
||||||
while True:
|
implements(ICredentialsChecker)
|
||||||
# Read a line
|
credentialInterfaces = (credentials.IUsernamePassword,
|
||||||
data = (yield from client_reader.readline())
|
credentials.IUsernameHashedPassword)
|
||||||
# Send it back to the client
|
|
||||||
client_writer.write(data)
|
def __init__(self, dbs):
|
||||||
|
self.dbs = dbs
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
server = loop.run_until_complete(asyncio.start_server(client_connected_handler, 'localhost', 2222))
|
def _cbPasswordMatch(self, matched, username):
|
||||||
try:
|
if matched:
|
||||||
loop.run_forever()
|
return username
|
||||||
finally:
|
else:
|
||||||
loop.close()
|
return failure.Failure(error.UnauthorizedLogin())
|
||||||
|
|
||||||
|
def requestAvatarId(self, credentials):
|
||||||
|
found_user = False
|
||||||
|
for db in self.dbs:
|
||||||
|
found_user = db.user_exist(credentials.username)
|
||||||
|
if found_user:
|
||||||
|
pwdfunc = db.check_pw
|
||||||
|
break
|
||||||
|
if found_user:
|
||||||
|
return defer.maybeDeferred(
|
||||||
|
pwdfunc, [credentials.username, credentials.password]).addCallback(
|
||||||
|
self._cbPasswordMatch, str(credentials.username))
|
||||||
|
else:
|
||||||
|
return defer.fail(error.UnauthorizedLogin())
|
||||||
|
|
||||||
|
checker = CredChecker(dbs)
|
||||||
|
smtp_portal.registerChecker(checker)
|
||||||
|
smtp_factory = SerpentSMTPFactory(smtp_portal)
|
||||||
|
imap_portal.registerChecker(checker)
|
||||||
|
imap_factory = SerpentIMAPFactory(imap_portal)
|
||||||
|
|
||||||
|
reactor.listenTCP(2500, smtp_factory)
|
||||||
|
reactor.listenTCP(1430, imap_factory)
|
||||||
|
|
||||||
|
qtask = LoopingCall(squeue.run)
|
||||||
|
qtask.start(conf.smtp_queue_check_period)
|
||||||
|
|
||||||
|
reactor.run()
|
|
@ -0,0 +1,329 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from twisted.cred import portal
|
||||||
|
from twisted.internet import protocol, ssl
|
||||||
|
from twisted.mail import imap4
|
||||||
|
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent.imap.mailbox import IMAPMailbox
|
||||||
|
from serpent.misc import IMAP_HDELIM, IMAP_MBOX_REG, IMAP_ACC_CONN_NUM
|
||||||
|
from shutil import rmtree, move
|
||||||
|
|
||||||
|
class IMAPUserAccount(object):
|
||||||
|
implements(imap4.IAccount)
|
||||||
|
|
||||||
|
def __init__(self, mdir):
|
||||||
|
if not os.path.exists(mdir):
|
||||||
|
os.makedirs(mdir)
|
||||||
|
self.dir = mdir
|
||||||
|
if self.dir in IMAP_MBOX_REG.keys():
|
||||||
|
IMAP_MBOX_REG[self.dir][IMAP_ACC_CONN_NUM] += 1
|
||||||
|
else:
|
||||||
|
IMAP_MBOX_REG[self.dir] = {}
|
||||||
|
IMAP_MBOX_REG[self.dir][IMAP_ACC_CONN_NUM] = 0
|
||||||
|
for m in conf.imap_auto_mbox.keys():
|
||||||
|
name = m
|
||||||
|
if isinstance(m, unicode):
|
||||||
|
m = m.encode('imap4-utf-7')
|
||||||
|
if m not in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
IMAP_MBOX_REG[self.dir][m] = self.create(m)
|
||||||
|
IMAP_MBOX_REG[self.dir][m].setSpecial(conf.imap_auto_mbox[name])
|
||||||
|
IMAP_MBOX_REG[self.dir][m]._start_monitor()
|
||||||
|
self.subscribe(m)
|
||||||
|
|
||||||
|
def _getMailbox(self, path):
|
||||||
|
if isinstance(path, unicode):
|
||||||
|
path = path.encode('imap4-utf-7')
|
||||||
|
fullPath = os.path.join(self.dir, path)
|
||||||
|
mbox = IMAPMailbox(fullPath)
|
||||||
|
mbox._start_monitor()
|
||||||
|
return mbox
|
||||||
|
|
||||||
|
def listMailboxes(self, ref, wildcard):
|
||||||
|
for box in os.listdir(self.dir):
|
||||||
|
yield box.decode('imap4-utf-7'), self.create(box)
|
||||||
|
|
||||||
|
def select(self, path, rw=False):
|
||||||
|
if isinstance(path, unicode):
|
||||||
|
path = path.encode('imap4-utf-7')
|
||||||
|
if path in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
return IMAP_MBOX_REG[self.dir][path]
|
||||||
|
else:
|
||||||
|
if path in os.listdir(self.dir):
|
||||||
|
return self.create(path)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def addMailbox(self, name, mbox = None):
|
||||||
|
if mbox:
|
||||||
|
raise NotImplementedError
|
||||||
|
return self.create(name)
|
||||||
|
|
||||||
|
def create(self, pathspec):
|
||||||
|
if isinstance(pathspec, unicode):
|
||||||
|
pathspec = pathspec.encode('imap4-utf-7')
|
||||||
|
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
paths = filter(None, pathspec.split(IMAP_HDELIM))
|
||||||
|
for accum in range(1, len(paths)):
|
||||||
|
subpath = IMAP_HDELIM.join(paths[:accum])
|
||||||
|
if subpath not in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
try:
|
||||||
|
IMAP_MBOX_REG[self.dir][subpath] = self._getMailbox(IMAP_HDELIM.join(paths[:accum]))
|
||||||
|
IMAP_MBOX_REG[self.dir][subpath].subscribe()
|
||||||
|
except imap4.MailboxCollision:
|
||||||
|
pass
|
||||||
|
IMAP_MBOX_REG[self.dir][subpath].hasChildren()
|
||||||
|
IMAP_MBOX_REG[self.dir][pathspec] = self._getMailbox(pathspec)
|
||||||
|
IMAP_MBOX_REG[self.dir][pathspec].hasNoChildren()
|
||||||
|
IMAP_MBOX_REG[self.dir][pathspec].subscribe()
|
||||||
|
return IMAP_MBOX_REG[self.dir][pathspec]
|
||||||
|
|
||||||
|
def delete(self, pathspec):
|
||||||
|
if isinstance(pathspec, unicode):
|
||||||
|
pathspec = pathspec.encode('imap4-utf-7')
|
||||||
|
if pathspec in conf.imap_auto_mbox.keys():
|
||||||
|
raise imap4.MailboxException, pathspec
|
||||||
|
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
raise imap4.NoSuchMailbox, pathspec
|
||||||
|
inferiors = self._inferiorNames(pathspec)
|
||||||
|
if r'\Noselect' in IMAP_MBOX_REG[self.dir][pathspec].getFlags():
|
||||||
|
# Check for hierarchically inferior mailboxes with this one
|
||||||
|
# as part of their root.
|
||||||
|
for inferior in inferiors:
|
||||||
|
if inferior != pathspec:
|
||||||
|
raise imap4.MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
|
||||||
|
for inferior in inferiors:
|
||||||
|
mdir = IMAP_MBOX_REG[self.dir][inferior].path
|
||||||
|
IMAP_MBOX_REG[self.dir][inferior].destroy()
|
||||||
|
del IMAP_MBOX_REG[self.dir][inferior]
|
||||||
|
rmtree(mdir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def rename(self, oldname, newname):
|
||||||
|
if oldname in conf.imap_auto_mbox.keys():
|
||||||
|
raise imap4.MailboxException, oldname
|
||||||
|
if isinstance(oldname, unicode):
|
||||||
|
oldname = oldname.encode('imap4-utf-7')
|
||||||
|
if isinstance(newname, unicode):
|
||||||
|
newname = newname.encode('imap4-utf-7')
|
||||||
|
if oldname not in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
raise imap4.NoSuchMailbox, oldname
|
||||||
|
inferiors = [(o, o.replace(oldname, newname, 1)) for o in self._inferiorNames(oldname)]
|
||||||
|
for (old, new) in inferiors:
|
||||||
|
if new in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
raise imap4.MailboxCollision, new
|
||||||
|
for (old, new) in inferiors:
|
||||||
|
IMAP_MBOX_REG[self.dir][old]._stop_monitor()
|
||||||
|
move(os.path.join(self.dir, old), os.path.join(self.dir, new))
|
||||||
|
IMAP_MBOX_REG[self.dir][new] = IMAP_MBOX_REG[self.dir][old]
|
||||||
|
IMAP_MBOX_REG[self.dir][new].path = os.path.join(self.dir, new)
|
||||||
|
IMAP_MBOX_REG[self.dir][new].open_flags()
|
||||||
|
IMAP_MBOX_REG[self.dir][new]._start_monitor()
|
||||||
|
del IMAP_MBOX_REG[self.dir][old]
|
||||||
|
return True
|
||||||
|
|
||||||
|
def subscribe(self, name):
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
name = name.encode('imap4-utf-7')
|
||||||
|
if name in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
IMAP_MBOX_REG[self.dir][name].subscribe()
|
||||||
|
return True
|
||||||
|
#raise imap4.NoSuchMailbox, name
|
||||||
|
|
||||||
|
def unsubscribe(self, name):
|
||||||
|
if name in conf.imap_auto_mbox.keys():
|
||||||
|
return False
|
||||||
|
# raise imap4.MailboxException, name
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
name = name.encode('imap4-utf-7')
|
||||||
|
if name in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
IMAP_MBOX_REG[self.dir][name].unsubscribe()
|
||||||
|
return True
|
||||||
|
#raise imap4.NoSuchMailbox, name
|
||||||
|
|
||||||
|
def isSubscribed(self, name):
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
name = name.encode('imap4-utf-7')
|
||||||
|
if name in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
return IMAP_MBOX_REG[self.dir][name].is_subscribed()
|
||||||
|
else:
|
||||||
|
raise imap4.NoSuchMailbox, name
|
||||||
|
|
||||||
|
def _inferiorNames(self, name):
|
||||||
|
name_l = name.split(IMAP_HDELIM)
|
||||||
|
inferiors = []
|
||||||
|
for infname in IMAP_MBOX_REG[self.dir].keys():
|
||||||
|
if name_l == infname.split(IMAP_HDELIM)[:len(name_l)]:
|
||||||
|
inferiors.append(infname)
|
||||||
|
return inferiors
|
||||||
|
|
||||||
|
class SerpentIMAPRealm(object):
|
||||||
|
implements(portal.IRealm)
|
||||||
|
|
||||||
|
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||||
|
if imap4.IAccount not in interfaces:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"This realm only supports the imap4.IAccount interface.")
|
||||||
|
mdir = os.path.join(conf.app_dir, conf.maildir_user_path % avatarId)
|
||||||
|
avatar = IMAPUserAccount(mdir)
|
||||||
|
return imap4.IAccount, avatar, lambda: None
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
class IMAPServerProtocol(imap4.IMAP4Server):
|
||||||
|
def lineReceived(self, line):
|
||||||
|
if isinstance(line, unicode):
|
||||||
|
line = line.encode('utf-8')
|
||||||
|
print "CLIENT:", line
|
||||||
|
imap4.IMAP4Server.lineReceived(self, line)
|
||||||
|
|
||||||
|
def sendLine(self, line):
|
||||||
|
imap4.IMAP4Server.sendLine(self, line)
|
||||||
|
if isinstance(line, unicode):
|
||||||
|
line = line.encode('utf-8')
|
||||||
|
print "SERVER:", line
|
||||||
|
|
||||||
|
def connectionLost(self, reason):
|
||||||
|
self.setTimeout(None)
|
||||||
|
if self.account and self.account.dir in IMAP_MBOX_REG.keys():
|
||||||
|
IMAP_MBOX_REG[self.account.dir][IMAP_ACC_CONN_NUM] -= 1
|
||||||
|
if IMAP_MBOX_REG[self.account.dir][IMAP_ACC_CONN_NUM] <= 0:
|
||||||
|
for m in IMAP_MBOX_REG[self.account.dir].keys():
|
||||||
|
if m == IMAP_ACC_CONN_NUM:
|
||||||
|
continue
|
||||||
|
IMAP_MBOX_REG[self.account.dir][m].close()
|
||||||
|
del IMAP_MBOX_REG[self.account.dir][m]
|
||||||
|
del IMAP_MBOX_REG[self.account.dir]
|
||||||
|
self.account = None
|
||||||
|
|
||||||
|
def _parseMbox(self, name):
|
||||||
|
if isinstance(name, unicode):
|
||||||
|
return name
|
||||||
|
try:
|
||||||
|
return name.decode('imap4-utf-7')
|
||||||
|
except:
|
||||||
|
#log.err()
|
||||||
|
raise imap4.IllegalMailboxEncoding(name)
|
||||||
|
|
||||||
|
def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
|
||||||
|
if not isinstance(mbox, IMAPMailbox):
|
||||||
|
self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
|
||||||
|
else:
|
||||||
|
imap4.maybeDeferred(self.mbox.fetch, messages, uid
|
||||||
|
).addCallback(self.__cbCopy, tag, mbox
|
||||||
|
).addCallback(self.__cbCopied, tag, mbox
|
||||||
|
).addErrback(self.__ebCopy, tag
|
||||||
|
)
|
||||||
|
|
||||||
|
def __cbCopy(self, messages, tag, mbox):
|
||||||
|
# XXX - This should handle failures with a rollback or something
|
||||||
|
addedDeferreds = []
|
||||||
|
fastCopyMbox = imap4.IMessageCopier(mbox, None)
|
||||||
|
for (_id, msg) in messages:
|
||||||
|
if fastCopyMbox is not None:
|
||||||
|
d = imap4.maybeDeferred(fastCopyMbox.copy, msg)
|
||||||
|
addedDeferreds.append(d)
|
||||||
|
continue
|
||||||
|
# XXX - The following should be an implementation of IMessageCopier.copy
|
||||||
|
# on an IMailbox->IMessageCopier adapter.
|
||||||
|
flags = msg.getFlags()
|
||||||
|
date = msg.getInternalDate()
|
||||||
|
body = imap4.IMessageFile(msg, None)
|
||||||
|
if body is not None:
|
||||||
|
bodyFile = body.open()
|
||||||
|
d = imap4.maybeDeferred(mbox.addMessage, bodyFile, flags, date)
|
||||||
|
else:
|
||||||
|
def rewind(f):
|
||||||
|
f.seek(0)
|
||||||
|
return f
|
||||||
|
_buffer = imap4.tempfile.TemporaryFile()
|
||||||
|
d = imap4.MessageProducer(msg, _buffer, self._scheduler
|
||||||
|
).beginProducing(None
|
||||||
|
).addCallback(lambda _, b=_buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
|
||||||
|
)
|
||||||
|
addedDeferreds.append(d)
|
||||||
|
return imap4.defer.DeferredList(addedDeferreds)
|
||||||
|
def __cbCopied(self, deferredIds, tag, mbox):
|
||||||
|
ids = []
|
||||||
|
failures = []
|
||||||
|
for (status, result) in deferredIds:
|
||||||
|
if status:
|
||||||
|
ids.append(result)
|
||||||
|
else:
|
||||||
|
failures.append(result.value)
|
||||||
|
if failures:
|
||||||
|
self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
|
||||||
|
else:
|
||||||
|
self.sendPositiveResponse(tag, 'COPY completed')
|
||||||
|
def __ebCopy(self, failure, tag):
|
||||||
|
self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
|
||||||
|
#log.err(failure)
|
||||||
|
|
||||||
|
def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
|
||||||
|
if not isinstance(mbox, IMAPMailbox):
|
||||||
|
self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
|
||||||
|
return
|
||||||
|
d = mbox.addMessage(message, flags, date)
|
||||||
|
d.addCallback(self.__cbAppend, tag, mbox)
|
||||||
|
d.addErrback(self.__ebAppend, tag)
|
||||||
|
|
||||||
|
def __cbAppend(self, result, tag, mbox):
|
||||||
|
self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
|
||||||
|
self.sendPositiveResponse(tag, 'APPEND complete')
|
||||||
|
def __ebAppend(self, failure, tag):
|
||||||
|
self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
|
||||||
|
|
||||||
|
def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
|
||||||
|
if isinstance(mbox, IMAPMailbox):
|
||||||
|
|
||||||
|
imap4.maybeDeferred(mbox.requestStatus, names).addCallbacks(
|
||||||
|
self.__cbStatus, self.__ebStatus,
|
||||||
|
(tag, mailbox), None, (tag, mailbox), None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.sendNegativeResponse(tag, "Could not open mailbox")
|
||||||
|
def _ebStatusGotMailbox(self, failure, tag):
|
||||||
|
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
|
||||||
|
#log.err(failure)
|
||||||
|
|
||||||
|
def __cbStatus(self, status, tag, box):
|
||||||
|
line = ' '.join(['%s %s' % x for x in status.iteritems()])
|
||||||
|
if isinstance(box, unicode):
|
||||||
|
box = box.encode('imap4-utf-7')
|
||||||
|
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
|
||||||
|
self.sendPositiveResponse(tag, 'STATUS complete')
|
||||||
|
def __ebStatus(self, failure, tag, box):
|
||||||
|
self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
|
||||||
|
def _cbListWork(self, mailboxes, tag, sub, cmdName):
|
||||||
|
for (name, box) in mailboxes:
|
||||||
|
if not sub or self.account.isSubscribed(name):
|
||||||
|
flags = box.getMboxFlags()
|
||||||
|
delim = box.getHierarchicalDelimiter()
|
||||||
|
resp = (imap4.DontQuoteMe(cmdName), map(imap4.DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
|
||||||
|
self.sendUntaggedResponse(imap4.collapseNestedLists(resp))
|
||||||
|
self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
class SerpentIMAPFactory(protocol.Factory):
|
||||||
|
def __init__(self, portal):
|
||||||
|
self.portal = portal
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
contextFactory = None
|
||||||
|
if conf.tls:
|
||||||
|
tls_data = file(conf.tls_pem, 'rb').read()
|
||||||
|
cert = ssl.PrivateCertificate.loadPEM(tls_data)
|
||||||
|
contextFactory = cert.options()
|
||||||
|
p = IMAPServerProtocol(contextFactory = contextFactory)
|
||||||
|
p.setTimeout(conf.imap_connection_timeout)
|
||||||
|
if conf.tls:
|
||||||
|
p.canStartTLS = True
|
||||||
|
p.IDENT = '%s ready' % conf.SRVNAME
|
||||||
|
p.portal = self.portal
|
||||||
|
return p
|
||||||
|
imap_portal = portal.Portal(SerpentIMAPRealm())
|
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent import rules
|
||||||
|
from serpent.queue import squeue
|
||||||
|
|
||||||
|
|
||||||
|
from email.Header import Header
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from twisted.internet import defer, ssl
|
||||||
|
from twisted.mail import smtp
|
||||||
|
from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
|
||||||
|
|
||||||
|
from twisted.cred.portal import IRealm, Portal
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SmtpMessageDelivery:
|
||||||
|
implements(smtp.IMessageDelivery)
|
||||||
|
|
||||||
|
def __init__(self, avatarId = None):
|
||||||
|
self.avatarId = avatarId
|
||||||
|
|
||||||
|
def receivedHeader(self, helo, origin, recipients):
|
||||||
|
header = conf.smtp_header.format(
|
||||||
|
sender_ip = helo[1],
|
||||||
|
sender_host = helo[0],
|
||||||
|
srv_host = conf.smtp_hostname,
|
||||||
|
srv_info = conf.srv_version,
|
||||||
|
sender = conf.smtp_email_delim.join([origin.local, origin.domain]),
|
||||||
|
id = self.messageid,
|
||||||
|
rcpt = conf.smtp_email_delim.join([recipients[0].dest.local, recipients[0].dest.domain]),
|
||||||
|
date = smtp.rfc822date()
|
||||||
|
)
|
||||||
|
return 'Received: %s' % Header(header)
|
||||||
|
|
||||||
|
def validateFrom(self, helo, origin): # Надо воткнуть всякие проверки хоста по HELO
|
||||||
|
try:
|
||||||
|
rules.validateFrom(self, [origin.local, origin.domain])
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return origin
|
||||||
|
|
||||||
|
def validateTo(self, user):
|
||||||
|
self.messageid = smtp.messageid().split('@')[0].strip('<')
|
||||||
|
try:
|
||||||
|
rules.validateTo(self, user)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
msg = {
|
||||||
|
'from': [user.orig.local, user.orig.domain],
|
||||||
|
'rcpt': [user.dest.local, user.dest.domain],
|
||||||
|
'transaction_id': self.messageid,
|
||||||
|
'id': smtp.messageid().split('@')[0].strip('<')
|
||||||
|
}
|
||||||
|
return lambda: SmtpMessage(msg)
|
||||||
|
|
||||||
|
class SmtpMessage:
|
||||||
|
implements(smtp.IMessage)
|
||||||
|
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.lines = []
|
||||||
|
self.size = 0
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def lineReceived(self, line):
|
||||||
|
self.lines.append(line)
|
||||||
|
|
||||||
|
def eomReceived(self):
|
||||||
|
self.lines.append('')
|
||||||
|
self.msg['message'] = "\n".join(self.lines)
|
||||||
|
self.lines = None
|
||||||
|
return defer.succeed(squeue.add(self.msg))
|
||||||
|
|
||||||
|
def connectionLost(self):
|
||||||
|
# There was an error, throw away the stored lines
|
||||||
|
self.lines = None
|
||||||
|
|
||||||
|
class SerpentESMTP(smtp.ESMTP):
|
||||||
|
def ext_AUTH(self, rest):
|
||||||
|
if self.canStartTLS and not self.startedTLS:
|
||||||
|
self.sendCode(538, 'Unencrypted auth denied')
|
||||||
|
return
|
||||||
|
if self.authenticated:
|
||||||
|
self.sendCode(503, 'Already authenticated')
|
||||||
|
return
|
||||||
|
parts = rest.split(None, 1)
|
||||||
|
chal = self.challengers.get(parts[0].upper(), lambda: None)()
|
||||||
|
if not chal:
|
||||||
|
self.sendCode(504, 'Unrecognized authentication type')
|
||||||
|
return
|
||||||
|
self.mode = smtp.AUTH
|
||||||
|
self.challenger = chal
|
||||||
|
if len(parts) > 1:
|
||||||
|
chal.getChallenge() # Discard it, apparently the client does not
|
||||||
|
# care about it.
|
||||||
|
rest = parts[1]
|
||||||
|
else:
|
||||||
|
rest = None
|
||||||
|
self.state_AUTH(rest)
|
||||||
|
|
||||||
|
class SerpentSMTPFactory(smtp.SMTPFactory):
|
||||||
|
protocol = SerpentESMTP
|
||||||
|
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
smtp.SMTPFactory.__init__(self, *a, **kw)
|
||||||
|
self.delivery = SmtpMessageDelivery()
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
contextFactory = None
|
||||||
|
if conf.tls:
|
||||||
|
tls_data = file(conf.tls_pem, 'rb').read()
|
||||||
|
cert = ssl.PrivateCertificate.loadPEM(tls_data)
|
||||||
|
contextFactory = cert.options()
|
||||||
|
p = smtp.SMTPFactory.buildProtocol(self, addr)
|
||||||
|
p.ctx = contextFactory
|
||||||
|
if conf.tls:
|
||||||
|
p.canStartTLS = True
|
||||||
|
p.host = conf.smtp_hostname
|
||||||
|
p.delivery = self.delivery
|
||||||
|
p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SmtpRealm:
|
||||||
|
implements(IRealm)
|
||||||
|
|
||||||
|
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||||
|
if smtp.IMessageDelivery in interfaces:
|
||||||
|
return smtp.IMessageDelivery, SmtpMessageDelivery(avatarId), lambda: None
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
smtp_portal = Portal(SmtpRealm())
|
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
class Config(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
conf = Config()
|
||||||
|
conf.VERSION = '0.1.0'
|
||||||
|
conf.SRVNAME = 'Serpent'
|
||||||
|
conf.srv_version = '%s %s' % (conf.SRVNAME, conf.VERSION)
|
||||||
|
conf.imap_connection_timeout = 60 * 30
|
||||||
|
conf.local_domains = ['dom.lan'] # Список доменов, для которых будет приниматься почта
|
||||||
|
conf.tls = True
|
||||||
|
conf.tls_pem = u'./serpent.pem'
|
||||||
|
conf.smtp_open_relay = False # Разрешить ли пересылку откуда угодно куда угодно
|
||||||
|
conf.smtp_email_delim = '@'
|
||||||
|
conf.smtp_header = '''from [{sender_ip}] (helo={sender_host})
|
||||||
|
by {srv_host} with ESMTP ({srv_info})
|
||||||
|
(envelope-from <{sender}>)
|
||||||
|
id {id}
|
||||||
|
for {rcpt}; {date}
|
||||||
|
'''
|
||||||
|
conf.smtp_hostname = 'mail.dom.lan'
|
||||||
|
conf.app_dir = '/home/inpos/tmp/serpent'
|
||||||
|
conf.smtp_queue_dir = 'smtp_queue'
|
||||||
|
conf.smtp_message_size = 40 # Размер в МБ
|
||||||
|
conf.smtp_queue_check_period = 30 # Период запуска обработки очереди в минутах
|
||||||
|
conf.smtp_queue_message_ttl = 3 * 24 * 60 # Время жизни сообщения в очереди в минутах
|
||||||
|
conf.maildir_user_path = 'mailstore/%s/'
|
||||||
|
conf.smtp_email_tls_required = True
|
||||||
|
|
||||||
|
conf.imap_SENT = 'Sent'
|
||||||
|
conf.imap_TRASH = 'Trash'
|
||||||
|
conf.imap_JUNK = 'Junk'
|
||||||
|
conf.imap_ARCHIVE = 'Archive'
|
||||||
|
conf.imap_DRAFTS = 'Drafts'
|
||||||
|
conf.imap_msg_info = 'msg_info.db'
|
||||||
|
conf.imap_mbox_info = 'mbox_info.db'
|
||||||
|
conf.imap_auto_mbox = {'INBOX': '\\INBOX',
|
||||||
|
conf.imap_SENT: '\\Sent',
|
||||||
|
conf.imap_TRASH: '\\Trash',
|
||||||
|
conf.imap_JUNK: '\\Junk',
|
||||||
|
conf.imap_ARCHIVE: '\\Archive',
|
||||||
|
conf.imap_DRAFTS: '\\Drafts'
|
||||||
|
}
|
||||||
|
conf.imap_expunge_on_close = True
|
||||||
|
conf.imap_check_new_interval = 10.0 # Период проверки новых сообщений в ящике
|
|
@ -0,0 +1,76 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
from glob import iglob
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent.misc import IMAP_FLAGS
|
||||||
|
|
||||||
|
class SmtpFileStore(object):
|
||||||
|
def __init__(self, dpath):
|
||||||
|
self.path = dpath
|
||||||
|
if not os.path.exists(dpath):
|
||||||
|
os.makedirs(dpath)
|
||||||
|
def read(self, fid):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.path, fid), 'rb') as f, open(os.path.join(self.path, fid + '.i'), 'rb') as i:
|
||||||
|
data = pickle.load(i)
|
||||||
|
try:
|
||||||
|
data['message'] = f.read()
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
return data
|
||||||
|
except:
|
||||||
|
#return False
|
||||||
|
raise
|
||||||
|
def write(self, data):
|
||||||
|
fid = data['id']
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.path, fid), 'wb') as f, open(os.path.join(self.path, fid + '.i'), 'wb') as i:
|
||||||
|
m = data['message']
|
||||||
|
data['message'] = ''
|
||||||
|
try:
|
||||||
|
pickle.dump(data, i, 2)
|
||||||
|
f.write(m)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
#return False
|
||||||
|
raise
|
||||||
|
def getinfo(self, fid):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.path, fid + '.i'), 'rb') as i:
|
||||||
|
data = pickle.load(i)
|
||||||
|
return data
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
def setinfo(self, data):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.path, data['id'] + '.i'), 'wb') as i:
|
||||||
|
pickle.dump(data, i, 2)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
def list(self):
|
||||||
|
return [i.split('/')[-1].rstrip('\.i') for i in iglob(self.path + '*.i')]
|
||||||
|
def delete(self, fid):
|
||||||
|
os.remove(os.path.join(self.path, fid + '.i'))
|
||||||
|
os.remove(os.path.join(self.path, fid))
|
||||||
|
|
||||||
|
class MailDirStore(object):
|
||||||
|
def __init__(self):
|
||||||
|
from serpent.imap import mailbox
|
||||||
|
self.mbox = mailbox
|
||||||
|
def deliver(self, user, message):
|
||||||
|
mdir = os.path.join(conf.app_dir, conf.maildir_user_path % user)
|
||||||
|
if not os.path.exists(mdir):
|
||||||
|
os.makedirs(mdir)
|
||||||
|
inbox = os.path.join(mdir, 'INBOX')
|
||||||
|
mailbox = self.mbox.IMAPMailbox(inbox)
|
||||||
|
try:
|
||||||
|
mailbox.addMessage(message, [IMAP_FLAGS['RECENT']])
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from twisted.mail import smtp
|
||||||
|
### Исключения
|
||||||
|
class SMTPAuthReqError(smtp.SMTPServerError):
|
||||||
|
'''Класс исключения. Сообщает о необходимости авторизации.'''
|
||||||
|
def __init__(self):
|
||||||
|
smtp.SMTPServerError.__init__(self, 550, 'Authentication required!')
|
||||||
|
|
||||||
|
class SMTPNotOpenRelay(smtp.SMTPServerError):
|
||||||
|
def __init__(self):
|
||||||
|
smtp.SMTPServerError.__init__(self, 550, 'Not Open Relay!')
|
||||||
|
###
|
||||||
|
|
||||||
|
SMTPBadRcpt = smtp.SMTPBadRcpt
|
||||||
|
SMTPBadSender = smtp.SMTPBadSender
|
|
@ -0,0 +1,371 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from twisted.mail import maildir, imap4
|
||||||
|
from twisted.mail.smtp import rfc822date
|
||||||
|
from twisted.internet import inotify
|
||||||
|
from twisted.python import filepath
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
import random
|
||||||
|
import email
|
||||||
|
|
||||||
|
from StringIO import StringIO
|
||||||
|
import os
|
||||||
|
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent import misc
|
||||||
|
|
||||||
|
from sqlitedict import SqliteDict
|
||||||
|
|
||||||
|
class LoopingTask(Thread):
|
||||||
|
def __init__(self, func, event, interval):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.func = func
|
||||||
|
self.interval = interval
|
||||||
|
self.stopped = event
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not self.stopped.wait(self.interval):
|
||||||
|
self.func()
|
||||||
|
|
||||||
|
class SerpentAppendMessageTask(maildir._MaildirMailboxAppendMessageTask):
|
||||||
|
|
||||||
|
def moveFileToNew(self):
|
||||||
|
while True:
|
||||||
|
newname = os.path.join(self.mbox.path, "new", maildir._generateMaildirName())
|
||||||
|
try:
|
||||||
|
self.osrename(self.tmpname, newname)
|
||||||
|
break
|
||||||
|
except OSError, (err, _):
|
||||||
|
import errno
|
||||||
|
# if the newname exists, retry with a new newname.
|
||||||
|
if err != errno.EEXIST:
|
||||||
|
self.fail()
|
||||||
|
newname = None
|
||||||
|
break
|
||||||
|
if newname is not None:
|
||||||
|
self.mbox.lastadded = newname
|
||||||
|
self.defer.callback(None)
|
||||||
|
self.defer = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedMaildir(maildir.MaildirMailbox):
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.list)
|
||||||
|
|
||||||
|
def __getitem__(self, i):
|
||||||
|
return self.list[i]
|
||||||
|
|
||||||
|
class IMAPMailbox(ExtendedMaildir):
|
||||||
|
implements(imap4.IMailbox, imap4.ICloseableMailbox)
|
||||||
|
|
||||||
|
AppendFactory = SerpentAppendMessageTask
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
maildir.initializeMaildir(path)
|
||||||
|
self.listeners = []
|
||||||
|
self.path = path
|
||||||
|
self.open_flags()
|
||||||
|
self.lastadded = None
|
||||||
|
self.__check_flags_()
|
||||||
|
|
||||||
|
def open_flags(self):
|
||||||
|
self.msg_info = SqliteDict(os.path.join(self.path, conf.imap_msg_info))
|
||||||
|
self.mbox_info = SqliteDict(os.path.join(self.path, conf.imap_mbox_info))
|
||||||
|
|
||||||
|
def _start_monitor(self):
|
||||||
|
self.notifier = inotify.INotify()
|
||||||
|
self.notifier.startReading()
|
||||||
|
self.notifier.watch(filepath.FilePath(os.path.join(self.path, 'new')),
|
||||||
|
callbacks=[self._new_files])
|
||||||
|
self.notifier.watch(filepath.FilePath(os.path.join(self.path,'cur')),
|
||||||
|
callbacks=[self._new_files])
|
||||||
|
|
||||||
|
def _stop_monitor(self):
|
||||||
|
self.notifier.stopReading()
|
||||||
|
self.notifier.loseConnection()
|
||||||
|
|
||||||
|
def _new_files(self, wo, path, code):
|
||||||
|
if code == inotify.IN_MOVED_TO or code == inotify.IN_DELETE:
|
||||||
|
for l in self.listeners:
|
||||||
|
l.newMessages(self.getMessageCount(), self.getRecentCount())
|
||||||
|
|
||||||
|
def __check_flags_(self):
|
||||||
|
if 'subscribed' not in self.mbox_info.keys(): self.mbox_info['subscribed'] = False
|
||||||
|
if 'flags' not in self.mbox_info.keys(): self.mbox_info['flags'] = []
|
||||||
|
if 'special' not in self.mbox_info.keys(): self.mbox_info['special'] = ''
|
||||||
|
if 'uidvalidity' not in self.mbox_info.keys(): self.mbox_info['uidvalidity'] = random.randint(0, 2**32)
|
||||||
|
if 'uidnext' not in self.mbox_info.keys(): self.mbox_info['uidnext'] = 1
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
l = [l for l in self.__msg_list_()]
|
||||||
|
for i in l:
|
||||||
|
fn = i.split('/')[-1]
|
||||||
|
if fn not in self.msg_info.keys():
|
||||||
|
val1 = {'uid': self.getUIDNext()}
|
||||||
|
if i.split('/')[-2] == 'new':
|
||||||
|
val1['flags'] = []
|
||||||
|
else:
|
||||||
|
val1['flags'] = [misc.IMAP_FLAGS['SEEN']]
|
||||||
|
self.msg_info[fn] = val1
|
||||||
|
#self.msg_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def subscribe(self):
|
||||||
|
self.mbox_info['subscribed'] = True
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def unsubscribe(self):
|
||||||
|
self.mbox_info['subscribed'] = False
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def is_subscribed(self):
|
||||||
|
return self.mbox_info['subscribed']
|
||||||
|
|
||||||
|
def __count_flagged_msgs_(self, flag):
|
||||||
|
val1 = [0 for fn in self.msg_info.keys() if flag in self.msg_info[fn]['flags']]
|
||||||
|
return len(val1)
|
||||||
|
|
||||||
|
def getHierarchicalDelimiter(self):
|
||||||
|
return misc.IMAP_HDELIM
|
||||||
|
|
||||||
|
def setSpecial(self, special):
|
||||||
|
self.mbox_info['special'] = special
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def getFlags(self):
|
||||||
|
return sorted(misc.IMAP_FLAGS.values())
|
||||||
|
|
||||||
|
def getMboxFlags(self):
|
||||||
|
f = list(self.mbox_info['flags'])
|
||||||
|
if self.mbox_info['special'] != '': f.append(self.mbox_info['special'])
|
||||||
|
return f
|
||||||
|
|
||||||
|
def addFlag(self, flag):
|
||||||
|
self.mbox_info['flags'] = list(set(self.mbox_info['flags']).union([flag]))
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def removeFlag(self, flag):
|
||||||
|
self.mbox_info['flags'] = list(set(self.mbox_info['flags']).difference([flag]))
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
|
||||||
|
def hasChildren(self):
|
||||||
|
flags = self.getFlags()
|
||||||
|
if misc.MBOX_FLAGS['HASCHILDREN'] not in flags:
|
||||||
|
self.addFlag(misc.MBOX_FLAGS['HASCHILDREN'])
|
||||||
|
if misc.MBOX_FLAGS['HASNOCHILDREN'] in flags:
|
||||||
|
self.removeFlag(misc.MBOX_FLAGS['HASNOCHILDREN'])
|
||||||
|
def hasNoChildren(self):
|
||||||
|
flags = self.getFlags()
|
||||||
|
if misc.MBOX_FLAGS['HASNOCHILDREN'] not in flags:
|
||||||
|
self.addFlag(misc.MBOX_FLAGS['HASNOCHILDREN'])
|
||||||
|
if misc.MBOX_FLAGS['HASCHILDREN'] in flags:
|
||||||
|
self.removeFlag(misc.MBOX_FLAGS['HASCHILDREN'])
|
||||||
|
|
||||||
|
def getMessageCount(self):
|
||||||
|
val1 = [0 for fn in self.msg_info.keys() if misc.IMAP_FLAGS['DELETED'] not in self.msg_info[fn]['flags']]
|
||||||
|
return len(val1)
|
||||||
|
|
||||||
|
def getRecentCount(self):
|
||||||
|
c = 0
|
||||||
|
for fn in self.msg_info.keys():
|
||||||
|
if misc.IMAP_FLAGS['RECENT'] in self.msg_info[fn]['flags']:
|
||||||
|
c += 1
|
||||||
|
info = self.msg_info[fn]
|
||||||
|
info['flags'] = set(info['flags']).difference(set([misc.IMAP_FLAGS['RECENT']]))
|
||||||
|
self.msg_info[fn] = info
|
||||||
|
#self.msg_info.commit(blocking=False) # XXX
|
||||||
|
return c
|
||||||
|
|
||||||
|
def getUnseenCount(self):
|
||||||
|
return self.getMessageCount() - self.__count_flagged_msgs_(misc.IMAP_FLAGS['SEEN'])
|
||||||
|
|
||||||
|
def isWriteable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getUIDValidity(self):
|
||||||
|
return self.mbox_info['uidvalidity']
|
||||||
|
|
||||||
|
def getUIDNext(self):
|
||||||
|
un = self.mbox_info['uidnext']
|
||||||
|
self.mbox_info['uidnext'] += 1
|
||||||
|
#self.mbox_info.commit(blocking=False) # XXX
|
||||||
|
return un
|
||||||
|
|
||||||
|
def getUID(self, num):
|
||||||
|
return num
|
||||||
|
|
||||||
|
def addMessage(self, message, flags = (), date = None):
|
||||||
|
return self.appendMessage(message).addCallback(self._cbAddMessage, flags)
|
||||||
|
|
||||||
|
def _cbAddMessage(self, obj, flags):
|
||||||
|
path = self.lastadded
|
||||||
|
self.lastadded = None
|
||||||
|
fn = path.split('/')[-1]
|
||||||
|
self.msg_info[fn] = {'uid': self.getUIDNext(), 'flags': flags}
|
||||||
|
#self.msg_info.commit(blocking=False) # XXX
|
||||||
|
if misc.IMAP_FLAGS['SEEN'] in flags and path.split('/')[-2] != 'cur':
|
||||||
|
new_path = os.path.join(self.path, 'cur', fn)
|
||||||
|
os.rename(path, new_path)
|
||||||
|
|
||||||
|
def __msg_list_(self):
|
||||||
|
a = []
|
||||||
|
for m in os.listdir(os.path.join(self.path, 'new')):
|
||||||
|
a.append(os.path.join(self.path, 'new', m))
|
||||||
|
for m in os.listdir(os.path.join(self.path, 'cur')):
|
||||||
|
a.append(os.path.join(self.path, 'cur', m))
|
||||||
|
return a
|
||||||
|
|
||||||
|
def _seqMessageSetToSeqDict(self, messageSet):
|
||||||
|
if not messageSet.last:
|
||||||
|
messageSet.last = self.getMessageCount()
|
||||||
|
|
||||||
|
seqMap = {}
|
||||||
|
msgs = self.__msg_list_()
|
||||||
|
for messageNum in messageSet:
|
||||||
|
if messageNum > 0 and messageNum <= self.getMessageCount():
|
||||||
|
seqMap[messageNum] = msgs[messageNum - 1]
|
||||||
|
return seqMap
|
||||||
|
|
||||||
|
def fetch(self, messages, uid):
|
||||||
|
return [[seq, MaildirMessage(seq,
|
||||||
|
file(filename, 'rb').read(),
|
||||||
|
self.msg_info[filename.split('/')[-1]]['flags'],
|
||||||
|
rfc822date())]
|
||||||
|
for seq, filename in self.__fetch_(messages, uid).iteritems()]
|
||||||
|
def __fetch_(self, messages, uid):
|
||||||
|
if uid:
|
||||||
|
messagesToFetch = {}
|
||||||
|
if not messages.last:
|
||||||
|
messages.last = self.mbox_info['uidnext']
|
||||||
|
fn_uid = dict((fn, self.msg_info[fn]['uid']) for fn in self.msg_info.keys())
|
||||||
|
for uid in messages:
|
||||||
|
if uid in fn_uid.values():
|
||||||
|
for name, _id in fn_uid.iteritems():
|
||||||
|
if uid == _id:
|
||||||
|
if os.path.exists(os.path.join(self.path,'new', name)):
|
||||||
|
messagesToFetch[uid] = os.path.join(self.path,'new', name)
|
||||||
|
elif os.path.exists(os.path.join(self.path,'cur', name)):
|
||||||
|
messagesToFetch[uid] = os.path.join(self.path,'cur', name)
|
||||||
|
else:
|
||||||
|
messagesToFetch = self._seqMessageSetToSeqDict(messages)
|
||||||
|
return messagesToFetch
|
||||||
|
def store(self, messages, flags, mode, uid):
|
||||||
|
d = {}
|
||||||
|
for _id, path in self.__fetch_(messages, uid).iteritems():
|
||||||
|
filename = path.split('/')[-1]
|
||||||
|
if mode < 0:
|
||||||
|
old_f = self.msg_info[filename]
|
||||||
|
old_f['flags'] = list(set(old_f['flags']).difference(set(flags)))
|
||||||
|
self.msg_info[filename] = old_f
|
||||||
|
if misc.IMAP_FLAGS['SEEN'] in flags and path.split('/')[-2] != 'new':
|
||||||
|
new_path = os.path.join(self.path, 'new', filename)
|
||||||
|
os.rename(path, new_path)
|
||||||
|
elif mode == 0:
|
||||||
|
old_f = self.msg_info[filename]
|
||||||
|
old_f['flags'] = flags
|
||||||
|
self.msg_info[filename] = old_f
|
||||||
|
elif mode > 0:
|
||||||
|
old_f = self.msg_info[filename]
|
||||||
|
old_f['flags'] = list(set(old_f['flags']).union(set(flags)))
|
||||||
|
self.msg_info[filename] = old_f
|
||||||
|
if misc.IMAP_FLAGS['SEEN'] in flags and path.split('/')[-2] != 'cur':
|
||||||
|
new_path = os.path.join(self.path, 'cur', filename)
|
||||||
|
os.rename(path, new_path)
|
||||||
|
d[_id] = self.msg_info[filename]['flags']
|
||||||
|
#self.msg_info.commit(blocking=False) # XXX
|
||||||
|
return d
|
||||||
|
|
||||||
|
def expunge(self):
|
||||||
|
uids = []
|
||||||
|
for path in self.__msg_list_():
|
||||||
|
fn = path.split('/')[-1]
|
||||||
|
if fn not in self.msg_info.keys():
|
||||||
|
continue
|
||||||
|
uid = self.msg_info[fn]['uid']
|
||||||
|
if misc.IMAP_FLAGS['DELETED'] in self.msg_info[fn]['flags']:
|
||||||
|
os.remove(path)
|
||||||
|
del self.msg_info[fn]
|
||||||
|
uids.append(uid)
|
||||||
|
#self.msg_info.commit(blocking=False) # XXX
|
||||||
|
return uids
|
||||||
|
|
||||||
|
def addListener(self, listener):
|
||||||
|
self.listeners.append(listener)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def removeListener(self, listener):
|
||||||
|
self.listeners.remove(listener)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def requestStatus(self, names):
|
||||||
|
return imap4.statusRequestHelper(self, names)
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
print('!!! %s - %d !!!' % (self.path, len(self.listeners)))
|
||||||
|
if len(self.listeners) == 0:
|
||||||
|
self._stop_monitor()
|
||||||
|
if conf.imap_expunge_on_close:
|
||||||
|
self.expunge()
|
||||||
|
self.msg_info.commit(blocking=False)
|
||||||
|
self.mbox_info.commit(blocking = False)
|
||||||
|
self.msg_info.close()
|
||||||
|
self.mbox_info.close()
|
||||||
|
|
||||||
|
class MaildirMessagePart(object):
|
||||||
|
implements(imap4.IMessagePart)
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
self.data = str(message)
|
||||||
|
|
||||||
|
def getHeaders(self, negate, *names):
|
||||||
|
if not names:
|
||||||
|
names = self.message.keys()
|
||||||
|
|
||||||
|
headers = {}
|
||||||
|
if negate:
|
||||||
|
for header in self.message.keys():
|
||||||
|
if header.upper() not in names:
|
||||||
|
headers[header.lower()] = self.message.get(header, '')
|
||||||
|
else:
|
||||||
|
for name in names:
|
||||||
|
headers[name.lower()] = self.message.get(name, '')
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def getBodyFile(self):
|
||||||
|
return StringIO(self.message.get_payload())
|
||||||
|
|
||||||
|
def getSize(self):
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
def isMultipart(self):
|
||||||
|
return self.message.is_multipart()
|
||||||
|
|
||||||
|
def getSubPart(self, part):
|
||||||
|
return MaildirMessagePart(self.message.get_payload(part))
|
||||||
|
|
||||||
|
class MaildirMessage(MaildirMessagePart):
|
||||||
|
implements(imap4.IMessage)
|
||||||
|
|
||||||
|
def __init__(self, uid, message, flags, date):
|
||||||
|
MaildirMessagePart.__init__(self, message)
|
||||||
|
self.uid = uid
|
||||||
|
self.message = email.message_from_string(message)
|
||||||
|
self.flags = flags
|
||||||
|
self.date = date
|
||||||
|
|
||||||
|
|
||||||
|
def getUID(self):
|
||||||
|
return self.uid
|
||||||
|
|
||||||
|
def getFlags(self):
|
||||||
|
return self.flags
|
||||||
|
|
||||||
|
def getInternalDate(self):
|
||||||
|
return self.date
|
|
@ -0,0 +1,23 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
MSG_ACTIVE = 0
|
||||||
|
MSG_FROZEN = 1
|
||||||
|
|
||||||
|
IMAP_FLAGS = {
|
||||||
|
'SEEN': '\\Seen',
|
||||||
|
'FLAGGED': '\\Flagged',
|
||||||
|
'ANSWERED': '\\Answered',
|
||||||
|
'RECENT': '\\Recent',
|
||||||
|
'DELETED': '\\Deleted',
|
||||||
|
'DRAFT': '\\Draft'
|
||||||
|
}
|
||||||
|
MBOX_FLAGS = {
|
||||||
|
'NOINFERIORS': '\\Noinferiors',
|
||||||
|
'NOSELECT': '\\Noselect',
|
||||||
|
'MARKED': '\\Marked',
|
||||||
|
'UNMARKED': '\\Unmarked',
|
||||||
|
'HASCHILDREN': '\\HasChildren',
|
||||||
|
'HASNOCHILDREN': '\\HasNoChildren'
|
||||||
|
}
|
||||||
|
IMAP_HDELIM = '.'
|
||||||
|
IMAP_ACC_CONN_NUM = '...ConnectionUUID...'
|
||||||
|
IMAP_MBOX_REG = {}
|
|
@ -0,0 +1,23 @@
|
||||||
|
######### Запуск приложения под twistd
|
||||||
|
|
||||||
|
from twisted.application import internet, service
|
||||||
|
from somemodule import EchoFactory
|
||||||
|
|
||||||
|
port = 7001
|
||||||
|
factory = EchoFactory()
|
||||||
|
|
||||||
|
# this is the important bit
|
||||||
|
application = service.Application("echo") # create the Application
|
||||||
|
echoService = internet.TCPServer(port, factory) # create the service
|
||||||
|
# add the service to the application
|
||||||
|
echoService.setServiceParent(application)
|
||||||
|
|
||||||
|
#############################################
|
||||||
|
def startService(self):
|
||||||
|
service.Service.startService(self)
|
||||||
|
|
||||||
|
def stopService(self):
|
||||||
|
service.Service.stopService(self)
|
||||||
|
if self._call:
|
||||||
|
self._call.cancel()
|
||||||
|
self._call = None
|
|
@ -0,0 +1,116 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent import dataio, misc, rules
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from os import path
|
||||||
|
from DNS import dnslookup
|
||||||
|
from operator import itemgetter
|
||||||
|
from smtplib import SMTP
|
||||||
|
|
||||||
|
class SmtpQueue(object):
|
||||||
|
def __init__(self, store, local_delivery):
|
||||||
|
self.stor = store
|
||||||
|
self.local_delivery = local_delivery
|
||||||
|
|
||||||
|
def add(self, data):
|
||||||
|
'''Ставит письмо в очередь'''
|
||||||
|
data['add_time'] = datetime.utcnow()
|
||||||
|
data['state'] = misc.MSG_ACTIVE
|
||||||
|
w = self.stor.write(data)
|
||||||
|
if not w:
|
||||||
|
return False
|
||||||
|
return self.__process_(data['id'])
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
'''Запускает обработку очереди'''
|
||||||
|
now = datetime.utcnow()
|
||||||
|
check_delta = timedelta(minutes = conf.smtp_queue_check_period)
|
||||||
|
expire_delta = timedelta(minutes = conf.smtp_queue_message_ttl)
|
||||||
|
for mid in self.__list_messages_():
|
||||||
|
info = self.stor.getinfo(mid)
|
||||||
|
if (now - info['add_time']) >= expire_delta:
|
||||||
|
self.stor.delete(mid)
|
||||||
|
continue
|
||||||
|
if (now - info['add_time']) >= check_delta:
|
||||||
|
continue
|
||||||
|
self.__process_(mid)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __local_deliver_(self, mid):
|
||||||
|
message = self.stor.read(mid)
|
||||||
|
user = rules.username_by_email(message['rcpt'])
|
||||||
|
return self.local_delivery.deliver(user, message['message'])
|
||||||
|
|
||||||
|
def __send_email_(self, mid):
|
||||||
|
info = self.stor.getinfo(mid)
|
||||||
|
try:
|
||||||
|
mail_servers = dnslookup(info['rcpt'][1], 'mx')
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
mail_servers = sorted(mail_servers, key=itemgetter(0))
|
||||||
|
for _, mx in mail_servers:
|
||||||
|
s = SMTP(local_hostname = conf.smtp_hostname)
|
||||||
|
try:
|
||||||
|
ret_code, banner = s.connect(mx, 25)
|
||||||
|
except:
|
||||||
|
s.quit()
|
||||||
|
continue
|
||||||
|
if ret_code != 220:
|
||||||
|
s.quit()
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
s.starttls()
|
||||||
|
except:
|
||||||
|
if conf.smtp_email_tls_required:
|
||||||
|
s.quit()
|
||||||
|
continue
|
||||||
|
from_addr = conf.smtp_email_delim.join(info['from'])
|
||||||
|
to_addr = conf.smtp_email_delim.join(info['rcpt'])
|
||||||
|
message = self.stor.read(mid)
|
||||||
|
try:
|
||||||
|
s.sendmail(from_addr, [to_addr], message['message'])
|
||||||
|
except:
|
||||||
|
s.quit()
|
||||||
|
continue
|
||||||
|
s.quit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def __freeze_(self, mid):
|
||||||
|
info = self.stor.getinfo(mid)
|
||||||
|
if info:
|
||||||
|
if info['state'] != misc.MSG_FROZEN :
|
||||||
|
info['state'] = misc.MSG_FROZEN
|
||||||
|
if self.stor.setinfo(info):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __process_(self, mid):
|
||||||
|
info = self.stor.getinfo(mid)
|
||||||
|
if info:
|
||||||
|
if info['rcpt'][1] in conf.local_domains:
|
||||||
|
if self.__local_deliver_(mid):
|
||||||
|
self.__remove_message_(mid)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.__freeze_(mid)
|
||||||
|
else:
|
||||||
|
if self.__send_email_(mid):
|
||||||
|
self.__remove_message_(mid)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.__freeze_(mid)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __list_messages_(self):
|
||||||
|
return self.stor.list()
|
||||||
|
|
||||||
|
def __remove_message_(self, mid):
|
||||||
|
self.stor.delete(mid)
|
||||||
|
|
||||||
|
mailstore = dataio.MailDirStore()
|
||||||
|
squeue = SmtpQueue(dataio.SmtpFileStore(path.join(conf.app_dir, conf.smtp_queue_dir)),
|
||||||
|
mailstore)
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from serpent.config import conf
|
||||||
|
from serpent.usrpwd import dbs
|
||||||
|
from serpent import errors
|
||||||
|
'''Здесь находятся функции для проверки различных вещей.'''
|
||||||
|
|
||||||
|
def validateFrom(obj, email):
|
||||||
|
if not conf.smtp_open_relay:
|
||||||
|
if email[1] in conf.local_domains:
|
||||||
|
if not obj.avatarId:
|
||||||
|
raise errors.SMTPAuthReqError()
|
||||||
|
elif obj.avatarId != username_by_email(email):
|
||||||
|
raise errors.SMTPBadSender(conf.smtp_email_delim.join(email))
|
||||||
|
elif obj.avatarId:
|
||||||
|
raise errors.SMTPBadSender(conf.smtp_email_delim.join(email))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def validateTo(obj, user):
|
||||||
|
local = user.dest.local
|
||||||
|
domain = user.dest.domain
|
||||||
|
for u, f in user.protocol._to:
|
||||||
|
if local == u.dest.local and domain == u.dest.domain:
|
||||||
|
del user.protocol._to[user.protocol._to.index((u,f))]
|
||||||
|
if domain in conf.local_domains and not username_by_email([local, domain]):
|
||||||
|
raise errors.SMTPBadRcpt(conf.smtp_email_delim.join([local, domain]))
|
||||||
|
if domain not in conf.local_domains and not obj.avatarId and not conf.smtp_open_relay:
|
||||||
|
raise errors.SMTPNotOpenRelay()
|
||||||
|
return True # Адрес найден в базах пользователей
|
||||||
|
|
||||||
|
def username_by_email(email):
|
||||||
|
result = None
|
||||||
|
for db in dbs:
|
||||||
|
result = db.username_by_email(email)
|
||||||
|
if result:
|
||||||
|
break
|
||||||
|
return result
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
userdb = {
|
||||||
|
'user1': ['password1', 'local1', 'dom.lan'],
|
||||||
|
'user2': ['password2', 'local2', 'dom.lan']
|
||||||
|
}
|
||||||
|
|
||||||
|
class DictUDB(object):
|
||||||
|
def __init__(self, userdb = userdb):
|
||||||
|
self. userdb = userdb
|
||||||
|
def username_by_email(self, email = None):
|
||||||
|
if not email:
|
||||||
|
return None
|
||||||
|
for usr in self.userdb.keys():
|
||||||
|
if email[0] == self.userdb[usr][1] and email[1] == self.userdb[usr][2]:
|
||||||
|
return usr
|
||||||
|
return False
|
||||||
|
def check_pw(self, creds):
|
||||||
|
usr = creds[0]
|
||||||
|
pwd = creds[1]
|
||||||
|
return usr in self.userdb.keys() and pwd == self.userdb[usr][0]
|
||||||
|
def user_exist(self, username):
|
||||||
|
return username in self.userdb.keys()
|
||||||
|
dbs = [DictUDB()]
|
Loading…
Reference in New Issue