Initial submission of working tool
authorPlamen Dimitrov <plamen.dimitrov@intra2net.com>
Thu, 12 Jul 2012 09:16:55 +0000 (11:16 +0200)
committerPlamen Dimitrov <plamen.dimitrov@intra2net.com>
Thu, 12 Jul 2012 09:16:55 +0000 (11:16 +0200)
imap_mark_seen.py [new file with mode: 0644]
imap_set_annotation.py [new file with mode: 0644]
mail_iterator.py [new file with mode: 0644]
warnings_handler.py [new file with mode: 0644]

diff --git a/imap_mark_seen.py b/imap_mark_seen.py
new file mode 100644 (file)
index 0000000..732d4a6
--- /dev/null
@@ -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 (file)
index 0000000..aaf52ae
--- /dev/null
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
+
+# 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 (file)
index 0000000..130d8ce
--- /dev/null
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
+
+#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 (file)
index 0000000..982299d
--- /dev/null
@@ -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