Compare commits

...

4 Commits
master ... bare

Author SHA1 Message Date
inpos 36b5f1971c ... 2016-05-30 14:44:56 +03:00
inpos 568d49d644 Начальный комит 2016-05-30 14:17:04 +03:00
inpos 7de6ed476a тестовый код 2016-05-30 09:08:13 +03:00
inpos 8b8b8c85fd Очистка 2016-05-29 22:43:16 +03:00
16 changed files with 71 additions and 1151 deletions

29
handlers/smtp.py 100644
View File

@ -0,0 +1,29 @@
from socketserver import StreamRequestHandler
ERR_CODE = {
220 : '',
500 : 'Syntax error, command unrecognized',
}
class SmtpProcessor(object):
def __init__(self, src_addr, rfile,wfile):
self.src_addr = src_addr
self.rfile = rfile
self.wfile = wfile
self.unrec_commands_count = 0
class SmtpHandler(StreamRequestHandler):
def handle(self):
# self.rfile is a file-like object created by the handler;
# we can now use e.g. readline() instead of raw recv() calls
self.data = self.rfile.readline().strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# Likewise, self.wfile is a file-like object used to write back
# to the client
self.wfile.write(self.data.upper())
#######################################################
processor = SmtpProcessor(self.client_address, self.rfile, self.wfile)

83
main.py
View File

@ -1,59 +1,38 @@
# -*- coding: utf-8 -*-
import socketserver, subprocess, sys
from threading import Thread
from pprint import pprint
import json
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
my_unix_command = ['bc']
HOST = 'localhost'
PORT = 2000
from zope.interface import implements
class SingleTCPHandler(socketserver.StreamRequestHandler):
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
def handle(self):
# self.rfile is a file-like object created by the handler;
# we can now use e.g. readline() instead of raw recv() calls
self.data = self.rfile.readline().strip()
print("{} wrote:".format(self.client_address[0]))
print(self.data)
# Likewise, self.wfile is a file-like object used to write back
# to the client
self.wfile.write(self.data.upper())
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)
class SimpleServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
# Ctrl-C will cleanly kill all spawned threads
daemon_threads = True
# much faster rebinding
allow_reuse_address = True
qtask = LoopingCall(squeue.run)
qtask.start(conf.smtp_queue_check_period)
def __init__(self, server_address, RequestHandlerClass):
socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass)
reactor.run()
if __name__ == "__main__":
server = SimpleServer((HOST, PORT), SingleTCPHandler)
# terminate with Ctrl-C
try:
server.serve_forever()
except KeyboardInterrupt:
sys.exit(0)

View File

@ -1,305 +0,0 @@
# -*- 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)
IMAP_MBOX_REG[self.dir][new].path_msg_info = os.path.join(self.dir, conf.imap_msg_info)
IMAP_MBOX_REG[self.dir][new].path_mbox_info = os.path.join(self.dir, conf.imap_mbox_info)
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()
def unsubscribe(self, name):
if name in conf.imap_auto_mbox:
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()
def isSubscribed(self, name):
if isinstance(name, unicode):
name = name.encode('imap4-utf-7')
return IMAP_MBOX_REG[self.dir][name].is_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):
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)))
################################################################################
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())

View File

@ -1,140 +0,0 @@
# -*- 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())

11
mechs/smtp.py 100644
View File

@ -0,0 +1,11 @@
import socketserver
from .. import handler.smtp as h_smtp
class SmtpServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
# Ctrl-C will cleanly kill all spawned threads
daemon_threads = True
# much faster rebinding
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass):
socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass)

View File

@ -1,36 +0,0 @@
# -*- 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.local_domains = ['dom.lan'] # Список доменов, для которых будет приниматься почта
conf.tls = True
conf.tls_pem = './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_subscribed = '.subscribed'
conf.imap_msg_info = 'msg_info.db'
conf.imap_mbox_info = 'mbox_info.db'
conf.imap_auto_mbox = ['INBOX', 'Sent', 'Trash']
conf.imap_expunge_on_close = True
conf.imap_check_new_interval = 10.0 # Период проверки новых сообщений в ящике

View File

@ -1,76 +0,0 @@
# -*- 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

View File

@ -1,15 +0,0 @@
# -*- 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

@ -1,313 +0,0 @@
# -*- 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
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, 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.msg_info = SqliteDict(os.path.join(path, conf.imap_msg_info))
self.mbox_info = SqliteDict(os.path.join(path, conf.imap_mbox_info))
self.lastadded = None
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 __check_flags_(self):
if 'subscribed' not in self.mbox_info.keys(): self.mbox_info['subscribed'] = False
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)
l = [l for l in self.__msg_list_()]
for i in l:
fn = i.split('/')[-1]
if fn not in 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)
def subscribe(self):
self.mbox_info['subscribed'] = True
self.mbox_info.commit(blocking=False)
def unsubscribe(self):
self.mbox_info['subscribed'] = False
self.mbox_info.commit(blocking=False)
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 getFlags(self):
return misc.IMAP_FLAGS.values()
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):
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):
return self.mbox_info['uidvalidity']
def getUIDNext(self):
un = self.mbox_info['uidnext']
self.mbox_info['uidnext'] += 1
self.mbox_info.commit(blocking=False)
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)
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]['flags']
self.msg_info[filename]['flags'] = 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.msg_info[filename]['flags'] = flags
elif mode > 0:
old_f = self.msg_info[filename]['flags']
self.msg_info[filename]['flags'] = 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)
d[_id] = self.msg_info[filename]['flags']
msg_info.commit(blocking=False)
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)
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

View File

@ -1,15 +0,0 @@
# -*- 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 = {}

View File

@ -1,23 +0,0 @@
######### Запуск приложения под 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

View File

@ -1,116 +0,0 @@
# -*- 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)

View File

@ -1,36 +0,0 @@
# -*- 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

View File

@ -1,24 +0,0 @@
# -*- 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()]