Compare commits

...

6 Commits

Author SHA1 Message Date
inpos 23adb48b90 ... 2016-06-10 16:30:49 +03:00
inpos 5ac9c36546 imap mech 2016-06-08 22:46:13 +03:00
inpos c98032ebe8 mbox 2016-06-08 22:34:54 +03:00
inpos d74db6bde5 dataio 2016-06-08 22:20:59 +03:00
inpos ee94d3a6d3 add mailbox.py 2016-06-08 20:42:39 +03:00
inpos 6812dd0b8d mailbox.py нам не нужен 2016-05-30 17:42:28 +03:00
6 changed files with 117 additions and 342 deletions

View File

@ -9,7 +9,7 @@ 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.mbox import ExtendedMaildir
from serpent.misc import IMAP_HDELIM, IMAP_MBOX_REG, IMAP_ACC_CONN_NUM
from shutil import rmtree, move
@ -25,11 +25,19 @@ class IMAPUserAccount(object):
else:
IMAP_MBOX_REG[self.dir] = {}
IMAP_MBOX_REG[self.dir][IMAP_ACC_CONN_NUM] = 0
for m in conf.imap_auto_mbox:
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():
<<<<<<< HEAD
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] = ExtendedMaildir(os.path.join(self.dir, m))
=======
IMAP_MBOX_REG[self.dir][m] = self.create(m)
IMAP_MBOX_REG[self.dir][m].setSpecial(conf.imap_auto_mbox[name])
>>>>>>> refs/remotes/origin/sqlitedict
IMAP_MBOX_REG[self.dir][m]._start_monitor()
self.subscribe(m)
@ -37,7 +45,7 @@ class IMAPUserAccount(object):
if isinstance(path, unicode):
path = path.encode('imap4-utf-7')
fullPath = os.path.join(self.dir, path)
mbox = IMAPMailbox(fullPath)
mbox = ExtendedMaildir(fullPath)
mbox._start_monitor()
return mbox
@ -54,7 +62,7 @@ class IMAPUserAccount(object):
if path in os.listdir(self.dir):
return self.create(path)
else:
raise imap4.NoSuchMailbox, path
return None
def addMailbox(self, name, mbox = None):
if mbox:
@ -71,18 +79,22 @@ class IMAPUserAccount(object):
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:
if pathspec in conf.imap_auto_mbox.keys():
raise imap4.MailboxException, pathspec
if pathspec not in IMAP_MBOX_REG[self.dir].keys():
raise imap4.MailboxException("No such mailbox")
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
@ -98,7 +110,7 @@ class IMAPUserAccount(object):
return True
def rename(self, oldname, newname):
if oldname in conf.imap_auto_mbox:
if oldname in conf.imap_auto_mbox.keys():
raise imap4.MailboxException, oldname
if isinstance(oldname, unicode):
oldname = oldname.encode('imap4-utf-7')
@ -111,11 +123,15 @@ class IMAPUserAccount(object):
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].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)
<<<<<<< HEAD
=======
IMAP_MBOX_REG[self.dir][new].open_flags()
IMAP_MBOX_REG[self.dir][new]._start_monitor()
>>>>>>> refs/remotes/origin/sqlitedict
del IMAP_MBOX_REG[self.dir][old]
return True
@ -124,19 +140,27 @@ class IMAPUserAccount(object):
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 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')
return IMAP_MBOX_REG[self.dir][name].is_subscribed()
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)
@ -283,6 +307,14 @@ class IMAPServerProtocol(imap4.IMAP4Server):
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,))
################################################################################
@ -297,6 +329,7 @@ class SerpentIMAPFactory(protocol.Factory):
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

View File

@ -6,9 +6,10 @@ conf = Config()
conf.VERSION = '0.1.0'
conf.SRVNAME = 'Serpent'
conf.srv_version = '%s %s' % (conf.SRVNAME, conf.VERSION)
conf.imap_connection_timeout = 120
conf.local_domains = ['dom.lan'] # Список доменов, для которых будет приниматься почта
conf.tls = True
conf.tls_pem = './serpent.pem'
conf.tls_pem = u'./serpent.pem'
conf.smtp_open_relay = False # Разрешить ли пересылку откуда угодно куда угодно
conf.smtp_email_delim = '@'
conf.smtp_header = '''from [{sender_ip}] (helo={sender_host})
@ -28,9 +29,17 @@ conf.smtp_email_tls_required = True
conf.imap_SENT = 'Sent'
conf.imap_TRASH = 'Trash'
conf.imap_subscribed = '.subscribed'
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', 'Sent', 'Trash']
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 # Период проверки новых сообщений в ящике

View File

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

View File

@ -1,315 +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)
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:
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

@ -0,0 +1,37 @@
from mailbox import Maildir
import os
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:
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:
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,13 +3,21 @@ 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'
}
MBOX_FLAGS = {
'NOINFERIORS': '\\Noinferiors',
'NOSELECT': '\\Noselect',
'MARKED': '\\Marked',
'UNMARKED': '\\Unmarked',
'HASCHILDREN': '\\HasChildren',
'HASNOCHILDREN': '\\HasNoChildren'
}
IMAP_HDELIM = '.'
IMAP_ACC_CONN_NUM = '...ConnectionNumber...'
IMAP_MBOX_REG = {}
IMAP_MBOX_REG = {}