diff --git a/mech_imap.py b/mech_imap.py index 7685993..ab9ac67 100644 --- a/mech_imap.py +++ b/mech_imap.py @@ -25,11 +25,13 @@ 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(): - 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] = self.create(m) + IMAP_MBOX_REG[self.dir][m].setSpecial(conf.imap_auto_mbox[name]) IMAP_MBOX_REG[self.dir][m]._start_monitor() self.subscribe(m) @@ -54,7 +56,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,15 +73,19 @@ 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.NoSuchMailbox, pathspec @@ -98,7 +104,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,13 +117,13 @@ class IMAPUserAccount(object): if new in IMAP_MBOX_REG[self.dir].keys(): 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() + 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] = self._getMailbox(new) - IMAP_MBOX_REG[self.dir][new].subscribe() + 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].open_flags() + IMAP_MBOX_REG[self.dir][new]._start_monitor() + del IMAP_MBOX_REG[self.dir][old] return True def subscribe(self, name): @@ -129,8 +135,9 @@ class IMAPUserAccount(object): #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(): @@ -141,7 +148,10 @@ class IMAPUserAccount(object): 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) @@ -288,6 +298,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,)) ################################################################################ @@ -302,6 +320,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 diff --git a/serpent/config.py b/serpent/config.py index 9f86679..b271977 100644 --- a/serpent/config.py +++ b/serpent/config.py @@ -6,6 +6,7 @@ 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 = u'./serpent.pem' @@ -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 # Период проверки новых сообщений в ящике \ No newline at end of file diff --git a/serpent/imap/mailbox.py b/serpent/imap/mailbox.py index a091bee..343071d 100644 --- a/serpent/imap/mailbox.py +++ b/serpent/imap/mailbox.py @@ -68,10 +68,13 @@ class IMAPMailbox(ExtendedMaildir): 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.open_flags() self.lastadded = None self.__check_flags_() + + 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)) def _start_monitor(self): self.notifier = inotify.INotify() @@ -81,6 +84,10 @@ class IMAPMailbox(ExtendedMaildir): self.notifier.watch(filepath.FilePath(os.path.join(self.path,'cur')), callbacks=[self._new_files]) + def _stop_monitor(self): + self.notifier.stopReading() + self.notifier.loseConnection() + def _new_files(self, wo, path, code): if code == inotify.IN_MOVED_TO or code == inotify.IN_DELETE: for l in self.listeners: @@ -88,6 +95,8 @@ class IMAPMailbox(ExtendedMaildir): def __check_flags_(self): if 'subscribed' not in self.mbox_info.keys(): self.mbox_info['subscribed'] = False + 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 self.mbox_info.commit(blocking=False) @@ -121,15 +130,53 @@ class IMAPMailbox(ExtendedMaildir): def getHierarchicalDelimiter(self): return misc.IMAP_HDELIM + def setSpecial(self, special): + self.mbox_info['special'] = special + self.mbox_info.commit(blocking=False) + def getFlags(self): - return misc.IMAP_FLAGS.values() + 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])) + self.mbox_info.commit(blocking=False) + + def removeFlag(self, flag): + self.mbox_info['flags'] = list(set(self.mbox_info['flags']).difference([flag])) + self.mbox_info.commit(blocking=False) + + 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']) 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']) + 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 + self.msg_info.commit(blocking=False) + return c def getUnseenCount(self): return self.getMessageCount() - self.__count_flagged_msgs_(misc.IMAP_FLAGS['SEEN']) @@ -209,16 +256,20 @@ class IMAPMailbox(ExtendedMaildir): 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))) + 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: - self.msg_info[filename]['flags'] = flags + old_f = self.msg_info[filename] + old_f['flags'] = flags + self.msg_info[filename] = old_f elif mode > 0: - old_f = self.msg_info[filename]['flags'] - self.msg_info[filename]['flags'] = list(set(old_f).union(set(flags))) + 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) @@ -255,12 +306,10 @@ class IMAPMailbox(ExtendedMaildir): 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() + if len(self.listeners) == 0: + self._stop_monitor() + if conf.imap_expunge_on_close: + self.expunge() class MaildirMessagePart(object): implements(imap4.IMessagePart) diff --git a/serpent/misc.py b/serpent/misc.py index f8960d0..df06832 100644 --- a/serpent/misc.py +++ b/serpent/misc.py @@ -10,6 +10,14 @@ IMAP_FLAGS = { 'DELETED': '\\Deleted', 'DRAFT': '\\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 = {} \ No newline at end of file