Compare commits

...

7 Commits

Author SHA1 Message Date
inpos 7318e52b94 zope.interface py3 2016-06-08 20:38:12 +03:00
inpos f2a43a948a mech_imap 2016-06-05 11:28:26 +03:00
inpos de41f90943 Возможность изменять флаги письма 2016-05-31 14:11:38 +03:00
inpos f36515f233 ExtendedMaildir 2016-05-31 13:42:31 +03:00
inpos 7423d4941d 2to3 2016-05-31 10:40:17 +03:00
inpos 44182ddd45 initial 2016-05-31 10:01:40 +03:00
inpos 6812dd0b8d mailbox.py нам не нужен 2016-05-30 17:42:28 +03:00
7 changed files with 89 additions and 381 deletions

View File

@ -6,7 +6,7 @@ from twisted.python import failure
from twisted.internet import reactor, defer
from twisted.internet.task import LoopingCall
from zope.interface import implements
from zope.interface import implementer
from mech_smtp import SerpentSMTPFactory, smtp_portal
from mech_imap import SerpentIMAPFactory, imap_portal
@ -14,10 +14,10 @@ from serpent.usrpwd import dbs
from serpent.queue import squeue
from serpent.config import conf
@implementer(ICredentialsChecker)
class CredChecker(object):
'''Класс проверки данных авторизации.
Параметром в конструктор передаётся список (list()) объектов баз пользователей.'''
implements(ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IUsernameHashedPassword)

View File

@ -2,20 +2,19 @@
import os
from zope.interface import implements
from zope.interface import implementer
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.imap.mailbox import ExtendedMaildir
from serpent.misc import IMAP_HDELIM, IMAP_MBOX_REG, IMAP_ACC_CONN_NUM
from shutil import rmtree, move
@implementer(imap4.IAccount)
class IMAPUserAccount(object):
implements(imap4.IAccount)
def __init__(self, mdir):
if not os.path.exists(mdir):
os.makedirs(mdir)
@ -27,17 +26,17 @@ class IMAPUserAccount(object):
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):
if isinstance(m, str):
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] = ExtendedMaildir(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):
if isinstance(path, str):
path = path.encode('imap4-utf-7')
fullPath = os.path.join(self.dir, path)
mbox = IMAPMailbox(fullPath)
mbox = ExtendedMaildir(fullPath)
mbox._start_monitor()
return mbox
@ -46,7 +45,7 @@ class IMAPUserAccount(object):
yield box.decode('imap4-utf-7'), self.create(box)
def select(self, path, rw=False):
if isinstance(path, unicode):
if isinstance(path, str):
path = path.encode('imap4-utf-7')
if path in IMAP_MBOX_REG[self.dir].keys():
return IMAP_MBOX_REG[self.dir][path]
@ -54,7 +53,7 @@ class IMAPUserAccount(object):
if path in os.listdir(self.dir):
return self.create(path)
else:
raise imap4.NoSuchMailbox, path
raise imap4.NoSuchMailbox(path)
def addMailbox(self, name, mbox = None):
if mbox:
@ -62,7 +61,7 @@ class IMAPUserAccount(object):
return self.create(name)
def create(self, pathspec):
if isinstance(pathspec, unicode):
if isinstance(pathspec, str):
pathspec = pathspec.encode('imap4-utf-7')
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
paths = filter(None, pathspec.split(IMAP_HDELIM))
@ -77,19 +76,19 @@ class IMAPUserAccount(object):
return IMAP_MBOX_REG[self.dir][pathspec]
def delete(self, pathspec):
if isinstance(pathspec, unicode):
if isinstance(pathspec, str):
pathspec = pathspec.encode('imap4-utf-7')
if pathspec in conf.imap_auto_mbox:
raise imap4.MailboxException, pathspec
raise imap4.MailboxException(pathspec)
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
raise imap4.NoSuchMailbox, pathspec
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"
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()
@ -99,47 +98,42 @@ class IMAPUserAccount(object):
def rename(self, oldname, newname):
if oldname in conf.imap_auto_mbox:
raise imap4.MailboxException, oldname
if isinstance(oldname, unicode):
raise imap4.MailboxException(oldname)
if isinstance(oldname, str):
oldname = oldname.encode('imap4-utf-7')
if isinstance(newname, unicode):
if isinstance(newname, str):
newname = newname.encode('imap4-utf-7')
if oldname not in IMAP_MBOX_REG[self.dir].keys():
raise imap4.NoSuchMailbox, oldname
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
raise imap4.MailboxCollision(new)
for (old, new) in inferiors:
m = IMAP_MBOX_REG[self.dir][old]
del IMAP_MBOX_REG[self.dir][old]
for l in m.listeners: m.listeners.remove(l)
m.close()
move(os.path.join(self.dir, old), os.path.join(self.dir, new))
IMAP_MBOX_REG[self.dir][new] = self._getMailbox(new)
IMAP_MBOX_REG[self.dir][new].subscribe()
return IMAP_MBOX_REG[self.dir][newname]
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):
if isinstance(name, str):
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:
raise imap4.MailboxException, name
if isinstance(name, unicode):
raise imap4.MailboxException(name)
if isinstance(name, str):
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):
if isinstance(name, str):
name = name.encode('imap4-utf-7')
return IMAP_MBOX_REG[self.dir][name].is_subscribed()
@ -151,9 +145,8 @@ class IMAPUserAccount(object):
inferiors.append(infname)
return inferiors
@implementer(portal.IRealm)
class SerpentIMAPRealm(object):
implements(portal.IRealm)
def requestAvatar(self, avatarId, mind, *interfaces):
if imap4.IAccount not in interfaces:
raise NotImplementedError(
@ -166,16 +159,16 @@ class SerpentIMAPRealm(object):
class IMAPServerProtocol(imap4.IMAP4Server):
def lineReceived(self, line):
if isinstance(line, unicode):
if isinstance(line, str):
line = line.encode('utf-8')
print "CLIENT:", line
print("CLIENT:", line)
imap4.IMAP4Server.lineReceived(self, line)
def sendLine(self, line):
imap4.IMAP4Server.sendLine(self, line)
if isinstance(line, unicode):
if isinstance(line, str):
line = line.encode('utf-8')
print "SERVER:", line
print("SERVER:", line)
def connectionLost(self, reason):
self.setTimeout(None)
@ -191,7 +184,7 @@ class IMAPServerProtocol(imap4.IMAP4Server):
self.account = None
def _parseMbox(self, name):
if isinstance(name, unicode):
if isinstance(name, str):
return name
try:
return name.decode('imap4-utf-7')
@ -282,7 +275,7 @@ class IMAPServerProtocol(imap4.IMAP4Server):
def __cbStatus(self, status, tag, box):
line = ' '.join(['%s %s' % x for x in status.iteritems()])
if isinstance(box, unicode):
if isinstance(box, str):
box = box.encode('imap4-utf-7')
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
self.sendPositiveResponse(tag, 'STATUS complete')
@ -298,7 +291,7 @@ class SerpentIMAPFactory(protocol.Factory):
def buildProtocol(self, addr):
contextFactory = None
if conf.tls:
tls_data = file(conf.tls_pem, 'rb').read()
tls_data = open(conf.tls_pem, 'rb').read()
cert = ssl.PrivateCertificate.loadPEM(tls_data)
contextFactory = cert.options()
p = IMAPServerProtocol(contextFactory = contextFactory)

View File

@ -5,8 +5,8 @@ from serpent import rules
from serpent.queue import squeue
from email.Header import Header
from zope.interface import implements
from email.header import Header
from zope.interface import implementer
from twisted.internet import defer, ssl
from twisted.mail import smtp
@ -16,10 +16,8 @@ from twisted.cred.portal import IRealm, Portal
@implementer(smtp.IMessageDelivery)
class SmtpMessageDelivery:
implements(smtp.IMessageDelivery)
def __init__(self, avatarId = None):
self.avatarId = avatarId
@ -59,9 +57,8 @@ class SmtpMessageDelivery:
}
return lambda: SmtpMessage(msg)
@implementer(smtp.IMessage)
class SmtpMessage:
implements(smtp.IMessage)
def __init__(self, msg):
self.lines = []
self.size = 0
@ -113,7 +110,7 @@ class SerpentSMTPFactory(smtp.SMTPFactory):
def buildProtocol(self, addr):
contextFactory = None
if conf.tls:
tls_data = file(conf.tls_pem, 'rb').read()
tls_data = open(conf.tls_pem, 'rb').read()
cert = ssl.PrivateCertificate.loadPEM(tls_data)
contextFactory = cert.options()
p = smtp.SMTPFactory.buildProtocol(self, addr)
@ -126,10 +123,8 @@ class SerpentSMTPFactory(smtp.SMTPFactory):
return p
@implementer(IRealm)
class SmtpRealm:
implements(IRealm)
def requestAvatar(self, avatarId, mind, *interfaces):
if smtp.IMessageDelivery in interfaces:
return smtp.IMessageDelivery, SmtpMessageDelivery(avatarId), lambda: None

View File

@ -4,7 +4,6 @@ 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):
@ -60,16 +59,17 @@ class SmtpFileStore(object):
class MailDirStore(object):
def __init__(self):
from serpent.imap import mailbox
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)
mailbox = self.mbox.Maildir(inbox)
msg = self.mbox.MaildirMessage(message)
try:
mailbox.addMessage(message, [IMAP_FLAGS['RECENT']])
mailbox.add(msg, [])
return True
except:
raise

View File

@ -1,317 +1,37 @@
# -*- 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
from mailbox import Maildir
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.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 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)
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)
class ExtendedMaildir(Maildir):
def set_flags(self, key, flags):
sflags = sorted(flags)
if sflags == self.get_flags(key): return True
subpath = self._lookup(key)
info = '2,' + ''.join(sflags)
oldpath = os.path.join(self._path, subpath)
newsubdir = os.path.split(subpath)[0]
newname = key + self.colon + info
if 'S' in sflags and newsubdir == 'new':
newsubdir = 'cur'
if 'S' not in sflags and newsubdir == 'cur':
newsubdir = 'new'
newpath = os.path.join(self._path, newsubdir, newname)
if hasattr(os, 'link'):
os.link(oldpath, newpath)
os.remove(oldpath)
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']
self.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)
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):
self.notifier.stopReading()
self.notifier.loseConnection()
if conf.imap_expunge_on_close:
self.expunge()
self.mbox_info.close()
self.msg_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, '')
os.rename(oldpath, newpath)
self._toc[key] = os.path.join(newsubdir, newname)
def get_flags(self, key):
subpath = self._lookup(key)
_, name = os.path.split(subpath)
info = name.split(self.colon)[-1]
if info.startswith('2,'):
return info[2:]
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
return ''
def add_flag(self, key, flag):
self.set_flags(key, ''.join(set(self.get_flags(key)) | set(flag)))
def remove_flag(self, key, flag):
if flag not in self.get_flags(key): return True
if self.get_flags(key):
self.set_flags(key, ''.join(set(self.get_flags(key)) - set(flag)))

View File

@ -3,12 +3,12 @@ MSG_ACTIVE = 0
MSG_FROZEN = 1
IMAP_FLAGS = {
'SEEN': '\\Seen',
'FLAGGED': '\\Flagged',
'ANSWERED': '\\Answered',
'RECENT': '\\Recent',
'DELETED': '\\Deleted',
'DRAFT': '\\Draft'
'S': '\\Seen',
'F': '\\Flagged',
'P': '\\Passed',
'R': '\\Replied',
'T': '\\Trashed',
'D': '\\Draft'
}
IMAP_HDELIM = '.'
IMAP_ACC_CONN_NUM = '...ConnectionNumber...'

View File

@ -51,7 +51,7 @@ class SmtpQueue(object):
for _, mx in mail_servers:
s = SMTP(local_hostname = conf.smtp_hostname)
try:
ret_code, banner = s.connect(mx, 25)
ret_code, _ = s.connect(mx, 25)
except:
s.quit()
continue