upload files

asyncio
inpos 2016-05-15 11:17:48 +03:00
parent 1e0b5d3813
commit 21225e2cdc
15 changed files with 1173 additions and 0 deletions

59
main.py 100644
View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from twisted.cred.checkers import ICredentialsChecker
from twisted.cred import credentials, error
from twisted.python import failure
from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall
from zope.interface import implements
from mech_smtp import SerpentSMTPFactory, smtp_portal
from mech_imap import SerpentIMAPFactory, imap_portal
from serpent.usrpwd import dbs
from serpent.queue import squeue
from serpent.config import conf
class CredChecker(object):
'''Класс проверки данных авторизации.
Параметром в конструктор передаётся список (list()) объектов баз пользователей.'''
implements(ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IUsernameHashedPassword)
def __init__(self, dbs):
self.dbs = dbs
def _cbPasswordMatch(self, matched, username):
if matched:
return username
else:
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()

299
mech_imap.py 100644
View File

@ -0,0 +1,299 @@
# -*- 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:
if m not in IMAP_MBOX_REG[self.dir].keys():
if isinstance(m, unicode):
m = m.encode('imap4-utf-7')
IMAP_MBOX_REG[self.dir][m] = IMAPMailbox(os.path.join(self.dir, m))
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:
raise imap4.NoSuchMailbox, path
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]))
except imap4.MailboxCollision:
pass
IMAP_MBOX_REG[self.dir][pathspec] = self._getMailbox(pathspec)
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:
raise imap4.MailboxException, pathspec
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
raise imap4.MailboxException("No such mailbox")
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:
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:
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)
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].flags['subscribed'] = True
IMAP_MBOX_REG[self.dir][name]._save_flags()
def unsubscribe(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].flags['subscribed'] = False
IMAP_MBOX_REG[self.dir][name]._save_flags()
def isSubscribed(self, name):
if isinstance(name, unicode):
name = name.encode('imap4-utf-7')
return IMAP_MBOX_REG[self.dir][name].flags['subscribed']
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):
print "CLIENT:", line
imap4.IMAP4Server.lineReceived(self, line)
def sendLine(self, line):
imap4.IMAP4Server.sendLine(self, line)
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)))
################################################################################
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)
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())

31
serpent.pem 100644
View File

@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIICizCCAfQCCQCUjWJ4YHqnSjANBgkqhkiG9w0BAQsFADCBiTELMAkGA1UEBhMC
UlUxDzANBgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRAwDgYDVQQKDAdN
QksgTGFiMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQbWFpbC5zZXJwZW50Lm9yZzEe
MBwGCSqGSIb3DQEJARYPc2FsZUBtYmstbGFiLnJ1MB4XDTE1MDQyMTEyMjAxNVoX
DTE2MDQyMDEyMjAxNVowgYkxCzAJBgNVBAYTAlJVMQ8wDQYDVQQIDAZNb3Njb3cx
DzANBgNVBAcMBk1vc2NvdzEQMA4GA1UECgwHTUJLIExhYjELMAkGA1UECwwCSVQx
GTAXBgNVBAMMEG1haWwuc2VycGVudC5vcmcxHjAcBgkqhkiG9w0BCQEWD3NhbGVA
bWJrLWxhYi5ydTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAmLEQkuDxufFO
keco1Wl8jmXMzp07EUFkuYcJYUAeIWSYmXPYs+fMVZS9FIA8jHahx67PgZqOgozY
Yan2VHBfU16H7OUl0GTz7je3rvnBd+WRiEgCVQuN3sM5bYy33DmqWyGJxXk1hFoH
oNJOhyM5nrNZsSO6omNcssfSn/gxZeMCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBE
KOW94kvNPvZC61Rwq3i3LErXH4TKJqltNDLyrKwScMx8qTPPMrr4mYWJaFpO2kiq
jsjNevUFIXrjbsycldIpNqHaqKEg2WdIdTGSClf2/Rkv2zzrCB4chbib6Mn1KuHF
syf1Ypa/BV6cb6jJxfIQljFz6IuZJz4zz2QnJfKROg==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCYsRCS4PG58U6R5yjVaXyOZczOnTsRQWS5hwlhQB4hZJiZc9iz
58xVlL0UgDyMdqHHrs+Bmo6CjNhhqfZUcF9TXofs5SXQZPPuN7eu+cF35ZGISAJV
C43ewzltjLfcOapbIYnFeTWEWgeg0k6HIzmes1mxI7qiY1yyx9Kf+DFl4wIDAQAB
AoGAFtlQQJp2sbuBZWXw/1aEtA5ZwoVWxHNDrludtLbSi26xQy1JvUovkpLqZHn4
FZDfDrGDDcLiFnkbHCpB2UrjiKwKSd9z24lqHOmGPbWH+nsj3molN8JtmJD4V89K
ClncRJ+TBQ2aj+vjmDozNJwnNvSc1sS2h8u1RMMe7A3a0NECQQDJ9446vOEaSGZH
7vTa83Igp3MCEmZ+52jJzEilm7SkATp0F17ARKG7ELJlpG1DLxHgoJLE+Ou/6ehN
9Ve88b35AkEAwYq1INLj7e6wA/VVFBWSn0ooHqkdXN75yKQFJOAgEb3yI3iztl3h
ifaxgOjivAxz7LOnKm/WsA4LSBe5WFTpuwJAP2QeFj2WgcNbpxRPcjGbDrjAFlRk
K0zCzSP7YU9/4UIpcKqtKLfh828IL3LugHnTqKd9qalfhXsLWPy6rylJMQJAYOuI
Rva1A6q65FCQGW2wLiqhqrD/rklPBsX0eYHvLVNUlaTVQicDUeaC/04gdRE7YDab
KOo2tZVi2uhefbiQDwJBAIfYntshq7z81u5PQF/qS4sNRZimtm5BtNx+FREheT4h
WaJMjtIrZAcg0VGiBRbIBqMz86lxqfe45nDlL97unuw=
-----END RSA PRIVATE KEY-----

View File

34
serpent/config.py 100644
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
class Config(object):
pass
conf = Config()
conf.VERSION = '0.0.1'
conf.SRVNAME = 'Serpent'
conf.srv_version = '%s %s' % (conf.SRVNAME, conf.VERSION)
conf.local_domains = ['dom.lan'] # Список доменов, для которых будет приниматься почта
conf.tls = True
conf.tls_pem = '/home/inpos/ownCloud/workspace/serpent/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.imap_SENT = 'Sent'
conf.imap_TRASH = 'Trash'
conf.imap_subscribed = '.subscribed'
conf.imap_flags = 'flags'
conf.imap_auto_mbox = ['INBOX', 'Sent', 'Trash']
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,341 @@
# -*- 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 pickle import load, dump
from StringIO import StringIO
import os
from serpent.config import conf
from serpent import misc
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, estr):
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.lastadded = None
if not os.path.exists(os.path.join(path, conf.imap_flags)):
self.__init_flags_()
else:
self.__load_flags_()
self.__check_flags()
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 _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 __init_flags_(self):
for fdir in ['new','cur']:
for fn in os.listdir(os.path.join(self.path, fdir)):
if fn not in self.flags['uid'].keys():
self.flags['uid'][fn] = self.getUIDNext()
if fdir == 'new':
self.flags['flags'][fn] = []
else:
self.flags['flags'][fn] = misc.IMAP_FLAGS['SEEN']
self._save_flags()
self.__load_flags_()
def __load_flags_(self):
try:
self.flags = load(file(os.path.join(self.path, conf.imap_flags), 'rb'))
except:
self.flags = {
'flags': {},
'subscribed': False,
'uidvalidity': random.randint(0, 2**32),
'uid': {},
'uidnext': 1
}
self._save_flags()
self.__init_flags_()
def __check_flags(self):
l = [l for l in self.__msg_list_()]
for i in l:
if i.split('/')[-1] not in self.flags['uid'].keys() or i.split('/')[-1] not in self.flags['flags'].keys():
if i.split('/')[-2] == 'new':
self.flags['flags'][i.split('/')[-1]] = []
else:
self.flags['flags'][i.split('/')[-1]] = misc.IMAP_FLAGS['SEEN']
self.flags['uid'][i.split('/')[-1]] = self.getUIDNext()
def _save_flags(self):
try:
with open(os.path.join(self.path, conf.imap_flags), 'wb') as f:
dump(self.flags, f, 2)
except:
pass
def __count_flagged_msgs_(self, flag):
c = 0
self.__load_flags_()
for dir in ['new','cur']:
for fn in os.listdir(os.path.join(self.path, dir)):
if flag in self.flags['flags'][fn]:
c += 1
return c
def getHierarchicalDelimiter(self):
return misc.IMAP_HDELIM
def getFlags(self):
return misc.IMAP_FLAGS.values()
def getMessageCount(self):
self.__load_flags_()
c = 0
c += len([n for n in self.__msg_list_() if misc.IMAP_FLAGS['DELETED'] not in self.flags['flags'][n.split('/')[-1]]])
return c
def getRecentCount(self):
return self.__count_flagged_msgs_(misc.IMAP_FLAGS['RECENT'])
def getUnseenCount(self):
return self.getMessageCount() - self.__count_flagged_msgs_(misc.IMAP_FLAGS['SEEN'])
def isWriteable(self):
return True
def getUIDValidity(self):
self.__load_flags_()
return self.flags['uidvalidity']
def getUIDNext(self):
self.__load_flags_()
self.flags['uidnext'] += 1
self._save_flags()
return self.flags['uidnext'] - 1
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):
self.__load_flags_()
path = self.lastadded
self.lastadded = None
fn = path.split('/')[-1]
self.flags['uid'][fn] = self.getUIDNext()
self.flags['flags'][fn] = flags
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)
self._save_flags()
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.flags['flags'][filename.split('/')[-1]],
rfc822date())]
for seq, filename in self.__fetch_(messages, uid).iteritems()]
def __fetch_(self, messages, uid):
self.__load_flags_()
if uid:
messagesToFetch = {}
if not messages.last:
messages.last = self.flags['uidnext']
for uid in messages:
if uid in self.flags['uid'].values():
for name, _id in self.flags['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):
self.__load_flags_()
d = {}
for _id, path in self.__fetch_(messages, uid).iteritems():
filename = path.split('/')[-1]
if mode < 0:
old_f = self.flags['flags'][filename]
self.flags['flags'][filename] = list(set(old_f).difference(set(flags)))
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:
self.flags["flags"][filename] = flags
elif mode > 0:
old_f = self.flags['flags'][filename]
self.flags['flags'][filename] = list(set(old_f).union(set(flags)))
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)
self._save_flags()
d[_id] = self.flags['flags'][filename]
return d
def expunge(self):
self.__load_flags_()
uids = []
for path in self.__msg_list_():
fn = path.split('/')[-1]
if fn not in self.flags['uid']:
continue
uid = self.flags['uid'][fn]
if misc.IMAP_FLAGS['DELETED'] in self.flags['flags'][fn]:
os.remove(path)
del self.flags['uid'][fn]
del self.flags['flags'][fn]
self._save_flags()
uids.append(uid)
return uids
def addListener(self, listener):
self.listeners.append(listener)
def removeListener(self, listener):
self.listeners.remove(listener)
def requestStatus(self, names):
return imap4.statusRequestHelper(self, names)
def destroy(self):
pass
def close(self):
self.notifier.stopReading()
self.notifier.loseConnection()
if conf.imap_expunge_on_close:
l = self.expunge()
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

15
serpent/misc.py 100644
View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
MSG_ACTIVE = 0
MSG_FROZEN = 1
IMAP_FLAGS = {
'SEEN': '\\Seen',
'FLAGGED': '\\Flagged',
'ANSWERED': '\\Answered',
'RECENT': '\\Recent',
'DELETED': '\\Deleted',
'DRAFT': '\\Draft'
}
IMAP_HDELIM = '.'
IMAP_ACC_CONN_NUM = '...ConnectionNumber...'
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

80
serpent/queue.py 100644
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from serpent.config import conf
from serpent import dataio, misc, rules
from datetime import datetime, timedelta
from os import path
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):
pass
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()]