306 lines
12 KiB
Python
306 lines
12 KiB
Python
# -*- 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, 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]._start_monitor()
|
|
self.subscribe(m)
|
|
|
|
def _getMailbox(self, path):
|
|
if isinstance(path, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
oldname = oldname.encode('imap4-utf-7')
|
|
if isinstance(newname, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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, str):
|
|
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 = open(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())
|