serpent/serpent/imap/mailbox.py

371 lines
13 KiB
Python
Raw Normal View History

2016-05-15 11:17:48 +03:00
# -*- 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
2016-05-30 17:41:12 +03:00
2016-05-15 11:17:48 +03:00
from StringIO import StringIO
import os
from serpent.config import conf
from serpent import misc
2016-05-26 08:49:35 +03:00
from sqlitedict import SqliteDict
2016-05-15 11:17:48 +03:00
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
2016-05-30 17:41:12 +03:00
except OSError, (err, _):
2016-05-15 11:17:48 +03:00
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
2016-06-10 00:10:51 +03:00
self.open_flags()
2016-05-15 11:17:48 +03:00
self.lastadded = None
self.__check_flags_()
2016-06-10 00:10:51 +03:00
def open_flags(self):
self.msg_info = SqliteDict(os.path.join(self.path, conf.imap_msg_info))
self.mbox_info = SqliteDict(os.path.join(self.path, conf.imap_mbox_info))
2016-05-15 11:17:48 +03:00
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])
2016-06-10 00:10:51 +03:00
def _stop_monitor(self):
self.notifier.stopReading()
self.notifier.loseConnection()
2016-05-15 11:17:48 +03:00
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())
2016-05-26 12:47:26 +03:00
def __check_flags_(self):
if 'subscribed' not in self.mbox_info.keys(): self.mbox_info['subscribed'] = False
2016-06-10 00:10:51 +03:00
if 'flags' not in self.mbox_info.keys(): self.mbox_info['flags'] = []
if 'special' not in self.mbox_info.keys(): self.mbox_info['special'] = ''
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
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
l = [l for l in self.__msg_list_()]
for i in l:
fn = i.split('/')[-1]
2016-05-30 17:41:12 +03:00
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
2016-06-10 17:31:19 +03:00
#self.msg_info.commit(blocking=False) # XXX
def subscribe(self):
self.mbox_info['subscribed'] = True
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
def unsubscribe(self):
self.mbox_info['subscribed'] = False
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
def is_subscribed(self):
return self.mbox_info['subscribed']
2016-05-26 08:49:35 +03:00
2016-05-15 11:17:48 +03:00
def __count_flagged_msgs_(self, flag):
val1 = [0 for fn in self.msg_info.keys() if flag in self.msg_info[fn]['flags']]
2016-05-26 12:47:26 +03:00
return len(val1)
2016-05-15 11:17:48 +03:00
def getHierarchicalDelimiter(self):
return misc.IMAP_HDELIM
2016-06-10 00:10:51 +03:00
def setSpecial(self, special):
self.mbox_info['special'] = special
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
2016-06-10 00:10:51 +03:00
2016-05-15 11:17:48 +03:00
def getFlags(self):
2016-06-10 00:10:51 +03:00
return sorted(misc.IMAP_FLAGS.values())
def getMboxFlags(self):
f = list(self.mbox_info['flags'])
if self.mbox_info['special'] != '': f.append(self.mbox_info['special'])
return f
def addFlag(self, flag):
self.mbox_info['flags'] = list(set(self.mbox_info['flags']).union([flag]))
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
2016-06-10 00:10:51 +03:00
def removeFlag(self, flag):
self.mbox_info['flags'] = list(set(self.mbox_info['flags']).difference([flag]))
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
2016-06-10 00:10:51 +03:00
def hasChildren(self):
flags = self.getFlags()
if misc.MBOX_FLAGS['HASCHILDREN'] not in flags:
self.addFlag(misc.MBOX_FLAGS['HASCHILDREN'])
if misc.MBOX_FLAGS['HASNOCHILDREN'] in flags:
self.removeFlag(misc.MBOX_FLAGS['HASNOCHILDREN'])
def hasNoChildren(self):
flags = self.getFlags()
if misc.MBOX_FLAGS['HASNOCHILDREN'] not in flags:
self.addFlag(misc.MBOX_FLAGS['HASNOCHILDREN'])
if misc.MBOX_FLAGS['HASCHILDREN'] in flags:
self.removeFlag(misc.MBOX_FLAGS['HASCHILDREN'])
2016-05-15 11:17:48 +03:00
def getMessageCount(self):
val1 = [0 for fn in self.msg_info.keys() if misc.IMAP_FLAGS['DELETED'] not in self.msg_info[fn]['flags']]
2016-05-26 12:47:26 +03:00
return len(val1)
2016-05-15 11:17:48 +03:00
def getRecentCount(self):
2016-06-10 00:10:51 +03:00
c = 0
for fn in self.msg_info.keys():
if misc.IMAP_FLAGS['RECENT'] in self.msg_info[fn]['flags']:
c += 1
info = self.msg_info[fn]
info['flags'] = set(info['flags']).difference(set([misc.IMAP_FLAGS['RECENT']]))
self.msg_info[fn] = info
2016-06-10 17:31:19 +03:00
#self.msg_info.commit(blocking=False) # XXX
2016-06-10 00:10:51 +03:00
return c
2016-05-15 11:17:48 +03:00
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']
2016-05-15 11:17:48 +03:00
def getUIDNext(self):
un = self.mbox_info['uidnext']
self.mbox_info['uidnext'] += 1
2016-06-10 17:31:19 +03:00
#self.mbox_info.commit(blocking=False) # XXX
2016-05-26 12:47:26 +03:00
return un
2016-05-15 11:17:48 +03:00
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}
2016-06-10 17:31:19 +03:00
#self.msg_info.commit(blocking=False) # XXX
2016-05-15 11:17:48 +03:00
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'],
2016-05-15 11:17:48 +03:00
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)
2016-05-15 11:17:48 +03:00
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:
2016-06-10 00:10:51 +03:00
old_f = self.msg_info[filename]
old_f['flags'] = list(set(old_f['flags']).difference(set(flags)))
self.msg_info[filename] = old_f
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:
2016-06-10 00:10:51 +03:00
old_f = self.msg_info[filename]
old_f['flags'] = flags
self.msg_info[filename] = old_f
elif mode > 0:
2016-06-10 00:10:51 +03:00
old_f = self.msg_info[filename]
old_f['flags'] = list(set(old_f['flags']).union(set(flags)))
self.msg_info[filename] = old_f
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']
2016-06-10 17:31:19 +03:00
#self.msg_info.commit(blocking=False) # XXX
2016-05-15 11:17:48 +03:00
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)
2016-06-10 17:31:19 +03:00
#self.msg_info.commit(blocking=False) # XXX
2016-05-15 11:17:48 +03:00
return uids
def addListener(self, listener):
self.listeners.append(listener)
2016-05-30 12:03:15 +03:00
return True
2016-05-15 11:17:48 +03:00
def removeListener(self, listener):
self.listeners.remove(listener)
2016-05-30 12:03:15 +03:00
return True
2016-05-15 11:17:48 +03:00
def requestStatus(self, names):
return imap4.statusRequestHelper(self, names)
def destroy(self):
pass
def close(self):
2016-06-10 17:31:19 +03:00
print('!!! %s - %d !!!' % (self.path, len(self.listeners)))
2016-06-10 00:10:51 +03:00
if len(self.listeners) == 0:
self._stop_monitor()
if conf.imap_expunge_on_close:
self.expunge()
2016-06-10 17:31:19 +03:00
self.msg_info.commit(blocking=False)
self.mbox_info.commit(blocking = False)
self.msg_info.close()
self.mbox_info.close()
2016-05-15 11:17:48 +03:00
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