Compare commits

..

6 Commits

Author SHA1 Message Date
inpos ea035c1e54 uuid 2016-06-10 17:58:10 +03:00
inpos e60d89c1b6 ускорение 2016-06-10 17:31:19 +03:00
inpos 5e23c30797 Флаги 2016-06-10 00:10:51 +03:00
inpos 3b43af2b2f ... 2016-06-08 23:09:12 +03:00
inpos 9956b94649 проблема с rename 2016-05-30 17:41:12 +03:00
inpos 4547c85237 mailbox 2016-05-30 12:03:15 +03:00
14 changed files with 1257 additions and 31 deletions

90
main.py
View File

@ -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()

329
mech_imap.py 100644
View File

@ -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())

140
mech_smtp.py 100644
View File

@ -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())

45
serpent/config.py 100644
View File

@ -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 # Период проверки новых сообщений в ящике

76
serpent/dataio.py 100644
View File

@ -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

15
serpent/errors.py 100644
View File

@ -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

View File

View File

@ -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

23
serpent/misc.py 100644
View File

@ -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 = {}

23
serpent/notes 100644
View File

@ -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

116
serpent/queue.py 100644
View File

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

36
serpent/rules.py 100644
View File

@ -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

24
serpent/usrpwd.py 100644
View File

@ -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()]