From 8775866290d07a15c6c3e561f16f3d240b67e257 Mon Sep 17 00:00:00 2001 From: Plamen Dimitrov Date: Thu, 12 Jul 2012 11:16:55 +0200 Subject: [PATCH] Initial submission of working tool --- imap_mark_seen.py | 92 ++++++++++++++++++++++++++++++++++++++++++ imap_set_annotation.py | 92 ++++++++++++++++++++++++++++++++++++++++++ mail_iterator.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ warnings_handler.py | 35 ++++++++++++++++ 4 files changed, 324 insertions(+), 0 deletions(-) create mode 100644 imap_mark_seen.py create mode 100644 imap_set_annotation.py create mode 100644 mail_iterator.py create mode 100644 warnings_handler.py diff --git a/imap_mark_seen.py b/imap_mark_seen.py new file mode 100644 index 0000000..732d4a6 --- /dev/null +++ b/imap_mark_seen.py @@ -0,0 +1,92 @@ +''' +imap-mark-seen.py - Tool to mark all e-mails as seen + +Copyright (c) 2012 Intra2net AG +Author: Plamen Dimitrov and Thomas Jarosch +''' +import logging +import argparse, getpass +from mail_iterator import MailIterator +from warnings_handler import WarningsHandler + +# logging settings +LOG_FILENAME = "imap_mark_seen.log" +LOG_FILE_LEVEL = logging.DEBUG +LOG_SHELL_LEVEL = logging.INFO +LOG_UNCLEAN_EXIT_LEVEL = logging.WARNING + +def main(): + """Main function.""" + + # prepare configuration + args = configure_args() + warnings_handler = prepare_logger() + logging.info("Marking messages as seen from %s of %s", args.folder, args.user) + psw = getpass.getpass() + + # prepare simple mail iterator and iterate throug mailboxes + session = MailIterator(args.server, args.user, psw) + for mailbox in session: + if args.folder != "all folders" and ("INBOX/" + args.folder) not in mailbox[2]: + continue + try: + mail_ids = session.fetch_messages() + except UserWarning as ex: + logging.error(ex) + continue + for mid in mail_ids: + logging.debug("Setting message %s from mailbox %s as seen", + mid.decode('iso-8859-1'), mailbox[2]) + try: + session.update_message(mid.decode('iso-8859-1'), mailbox[2]) + except UserWarning as ex: + logging.error(ex) + continue + + logging.info("Finished marking messages as seen. Exiting with code %s.", warnings_handler.detected_problems) + return int(warnings_handler.detected_problems > 0) + +def configure_args(): + """Configure arguments and return them.""" + + # parse arguments + parser = argparse.ArgumentParser(description="Tool to mark messages as seen.") + parser.add_argument('-u', '--user', dest='user', action='store', + required=True, help='mark all messages as seen for a single user') + parser.add_argument('-f', '--folder', dest='folder', action='store', + default="all folders", help='only mark given folder as seen') + parser.add_argument('-s', '--server', dest='server', action='store', + default="localhost", help='imap server name with default localhost') + args = parser.parse_args() + + return args + +def prepare_logger(): + """Sets up the logging functionality""" + + # reset the log + with open(LOG_FILENAME, 'w'): + pass + + # add basic configuration + logging.basicConfig(filename=LOG_FILENAME, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=LOG_FILE_LEVEL) + + # add a handler for a console output + default_logger = logging.getLogger('') + console = logging.StreamHandler() + console.setLevel(LOG_SHELL_LEVEL) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console.setFormatter(formatter) + default_logger.addHandler(console) + + # add a handler for warnings counting + warnings_handler = WarningsHandler() + warnings_handler.setLevel(LOG_UNCLEAN_EXIT_LEVEL) + default_logger.addHandler(warnings_handler) + + return warnings_handler + +if __name__ == "__main__": + main() diff --git a/imap_set_annotation.py b/imap_set_annotation.py new file mode 100644 index 0000000..aaf52ae --- /dev/null +++ b/imap_set_annotation.py @@ -0,0 +1,92 @@ +"""Collection of functions for mass IMAP server manipulation""" +import imaplib +import logging +import re + +SERVER = 'intranator.m.i2n' +USERNAME = 'FIXME' +PASSWORD = 'FIXME' +ANNOTATION_SHARED_SEEN = '"/vendor/cmu/cyrus-imapd/sharedseen"' + +MAILBOX_RESP = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') + +# imaplib.Debug = 5 + +class ImapAction: + """Class for mass IMAP manipulation""" + + def __init__(self, server, username, password): + self.mail_con = None + self.mailboxes = None + + # connect to server + try: + self.mail_con = imaplib.IMAP4(server) + except Exception as ex: + raise UserWarning("Could not connect to host %s: %s" % (server, ex)) + + # log in + try: + self.mail_con.login(username, password) + logging.info("Logged in as %s.", username) + except: + self.logged_in = False + raise UserWarning("Could not log in as user " + username) + self.logged_in = True + + # list mailboxes + try: + _result, self.mailboxes = self.mail_con.list() + except (self.mail_con.error): + raise UserWarning("Could not retrieve mailboxes for user " + username) + + def mass_set_group_rights(self, identifier, rights): + """Mass set ACL rights on mailboxes""" + for raw_mailbox in self.mailboxes: + mailbox = MAILBOX_RESP.match(raw_mailbox.decode('iso-8859-1')).groups()[2] + if not mailbox.startswith('\"INBOX'): + print('Skipping mailbox %s' % mailbox) + continue + print('Modifying ACL for mailbox %s' % mailbox) + + self.mail_con.setacl(mailbox, identifier, rights) + + def mass_set_shared_seen(self): + """Enable shared seen state on all mailboxes""" + for raw_mailbox in self.mailboxes: + mailbox = MAILBOX_RESP.match(raw_mailbox.decode('iso-8859-1')).groups()[2] + if not mailbox.startswith('\"INBOX'): + print('Skipping mailbox %s' % mailbox) + continue + print('Setting sharedseen annotation on mailbox %s' % mailbox) + + self.mail_con.setannotation(mailbox, ANNOTATION_SHARED_SEEN, '("value.shared" "true")') + + def list_sharedseen_state(self): + """List shared seen state on all mailboxes""" + for raw_mailbox in self.mailboxes: + mailbox = MAILBOX_RESP.match(raw_mailbox.decode('iso-8859-1')).groups()[2] + if not mailbox.startswith('\"INBOX'): + print('Skipping mailbox %s' % mailbox) + continue + + raw_annotations = self.mail_con.getannotation(mailbox, ANNOTATION_SHARED_SEEN, '"value.shared"') + + # Hack to parse annotation result + sharedseen_enabled = False + if str(raw_annotations[1]).find('("value.shared" "true")') != -1: + sharedseen_enabled = True + + print('sharedseen state on mailbox %s: %s' % (mailbox, sharedseen_enabled)) + +def main(): + """Main function""" + imap = ImapAction(SERVER, USERNAME, PASSWORD) + + imap.mass_set_group_rights("group:infokonto", "lrswipcd") + imap.mass_set_shared_seen() + + imap.list_sharedseen_state() + +if(__name__ == "__main__"): + main() diff --git a/mail_iterator.py b/mail_iterator.py new file mode 100644 index 0000000..130d8ce --- /dev/null +++ b/mail_iterator.py @@ -0,0 +1,105 @@ +''' +mail_iterator.py - The module contains the MailIterator class. + +Copyright (c) 2012 Intra2net AG +Author: Plamen Dimitrov and Thomas Jarosch +''' + +import sys +import imaplib, socket +import re +import logging + +MAILBOX_RESP = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') + +#imaplib.Debug = 4 + +class MailIterator: + """This class communicates with the e-mail server.""" + + # class attributes + # IMAP4_SSL for connection with an IMAP server + mail_con = None + # list of tuples (uidvalidity, mailboxname) for the retrieved mailboxes + mailboxes = None + # logged in status + logged_in = None + + def __init__(self, server, username, password): + """Creates a connection and a user session.""" + + # connect to server + try: + self.mail_con = imaplib.IMAP4(server) + logging.info("Connected to %s", server) + except socket.error as ex: + logging.error("Could not connect to host: %s", ex) + sys.exit() + + # log in + try: + self.mail_con.login(username, password) + self.logged_in = True + logging.info("Logged in as %s.", username) + except self.mail_con.error as ex: + self.logged_in = False + logging.error("Could not log in as user %s: %s", username, ex) + sys.exit() + + # list mailboxes + try: + _result, mailboxes = self.mail_con.list() + except self.mail_con.error as ex: + logging.warning("Could not retrieve mailboxes for user %s: %s", username, ex) + self.mailboxes = [] + for mailbox in mailboxes: + mailbox = MAILBOX_RESP.match(mailbox.decode('iso-8859-1')).groups() + self.mailboxes.append(mailbox) + self.mailboxes = sorted(self.mailboxes, key=lambda box: box[2], reverse=True) + + return + + def __del__(self): + """Closes the connection and the user session.""" + + if(self.logged_in): + self.mail_con.close() + self.mail_con.logout() + + def __iter__(self): + """Iterates through all mailboxes, returns (children,delimiter,name).""" + + for mailbox in self.mailboxes: + logging.debug("Checking mailbox %s", mailbox[2]) + # select mailbox if writable + try: + self.mail_con.select(mailbox[2]) + logging.info("Processing mailbox %s", mailbox[2]) + except self.mail_con.readonly: + logging.warning("Mailbox %s is not writable and therefore skipped.", mailbox[2]) + continue + yield mailbox + + def fetch_messages(self): + """Fetches the messages from the current mailbox, return list of uids.""" + + try: + # Work around unsolicited server responses in imaplib by clearing them + self.mail_con.response('SEARCH') + _result, data = self.mail_con.uid('search', None, "ALL") + except (self.mail_con.error): + raise UserWarning("Could not fetch messages.") + mailid_list = data[0].split() + return mailid_list + + def update_message(self, mid, mailbox): + """Sets the \\Seen flag for a message.""" + + try: + # Work around unsolicited server responses in imaplib by clearing them + self.mail_con.response('STORE') + result, data = self.mail_con.uid('STORE', mid, '+FLAGS', "(\Seen)") + logging.debug("New flags for message %s are %s", mid, data) + except (self.mail_con.error): + raise UserWarning("Could not set the flags for the e-mail " + mid.decode('iso-8859-1') + ".") + self.mail_con.expunge() diff --git a/warnings_handler.py b/warnings_handler.py new file mode 100644 index 0000000..982299d --- /dev/null +++ b/warnings_handler.py @@ -0,0 +1,35 @@ +''' +restore-mail-inject.py - Tool to inject mails via IMAP + +Copyright (c) 2012 Intra2net AG +Author: Plamen Dimitrov and Thomas Jarosch + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +''' +import logging + +class WarningsHandler(logging.Handler): + """This class iterates through the e-mail files.""" + + # class attributes + detected_problems = None + + def __init__(self): + """Initialize a handler to count number of warnings.""" + + logging.Handler.__init__(self) + self.detected_problems = 0 + + def emit(self, record): + """Increase number of warnings found""" + + self.detected_problems += 1 + \ No newline at end of file -- 1.7.1