From 8f3294d7b5df9e3b9f1e1b6c3d561e0dd542ef9a Mon Sep 17 00:00:00 2001 From: Guillaume Seren Date: Sun, 25 Mar 2018 15:55:23 +0200 Subject: [PATCH 1/2] Cleanup MailMove class Fix multi import Cleanup flake8 error Refactor MailMover.move() logic Fix import order Cleanup class linter error --- afew/MailMover.py | 93 ++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/afew/MailMover.py b/afew/MailMover.py index de225564..bc8a3607 100644 --- a/afew/MailMover.py +++ b/afew/MailMover.py @@ -1,38 +1,42 @@ # -*- coding: utf-8 -*- # SPDX-License-Identifier: ISC # Copyright (c) dtk +"""This module contain the MailMover class""" -import notmuch +import os import logging -import os, shutil +import shutil +import sys from subprocess import check_call, CalledProcessError - -from .Database import Database -from .utils import get_message_summary from datetime import date, datetime, timedelta import uuid +import notmuch +from .Database import Database +from .utils import get_message_summary class MailMover(Database): ''' - Move mail files matching a given notmuch query into a target maildir folder. + Move mail files matching a given notmuch query to a target maildir folder. ''' def __init__(self, max_age=0, rename = False, dry_run=False, notmuch_args=''): - super().__init__() + super(MailMover, self).__init__() self.db = notmuch.Database(self.db_path) self.query = 'folder:"{folder}" AND {subquery}' if max_age: days = timedelta(int(max_age)) start = date.today() - days now = datetime.now() - self.query += ' AND {start}..{now}'.format(start=start.strftime('%s'), - now=now.strftime('%s')) + self.query += ' AND {start}..{now}'.format( + start=start.strftime('%s'), + now=now.strftime('%s')) self.dry_run = dry_run self.rename = rename self.notmuch_args = notmuch_args + """ return the new name """ def get_new_name(self, fname, destination): basename = os.path.basename(fname) submaildir = os.path.split(os.path.split(fname)[0])[1] @@ -53,7 +57,10 @@ def move(self, maildir, rules): ''' # identify and move messages logging.info("checking mails in '{}'".format(maildir)) - to_delete_fnames = [] + # Open DB R/W if are not in dry-run + if not self.dry_run: + self.db = notmuch.Database(self.db_path, + mode=notmuch.Database.MODE.READ_WRITE) moved = False for query in rules.keys(): destination = '{}/{}/'.format(self.db_path, rules[query]) @@ -63,7 +70,7 @@ def move(self, maildir, rules): messages = notmuch.Query(self.db, main_query).search_messages() for message in messages: # a single message (identified by Message-ID) can be in several - # places; only touch the one(s) that exists in this maildir + # places; only touch the one(s) that exists in this maildir all_message_fnames = message.get_filenames() to_move_fnames = [name for name in all_message_fnames if maildir in name] @@ -76,31 +83,45 @@ def move(self, maildir, rules): if self.dry_run: continue try: - shutil.copy2(fname, self.get_new_name(fname, destination)) - to_delete_fnames.append(fname) - except shutil.SameFileError: - logging.warn("trying to move '{}' onto itself".format(fname)) - continue + logging.info("Copy original mail: "+fname) + logging.info("Path of mail: "+os.path.dirname(fname)) + logging.info("Copy to: "+destination) + dbpath = self.db.get_path() + logging.info("Path of DB: "+dbpath) + relpath=os.path.relpath(os.path.dirname(destination), dbpath) + filename = os.path.basename(fname) + logging.info("filename: "+filename) + logging.info("Relative path DB -> file: " + relpath) + # 1 COPY + shutil.copy2(fname, + self.get_new_name(fname, + destination)) + dest_file_rel = dbpath+'/'+relpath+'/'+filename + logging.info("Add new copy to the DB: "+dest_file_rel) + # OPEN DB R/W + logging.info("Open DB R/W") + # print vars(self.db) + # 2 Add NEW MESSAGE TO DB + moved_message = notmuch.Database.add_message(self.db, dest_file_rel) + logging.info("Delete original mail file: "+fname) + os.remove(fname) + # 3 REMOVE ORIGINAL + logging.info("Delete the original mail in DB") + # 4 REMOVE ORIGINAL FROM DB + notmuch.Database.remove_message(self.db, fname) + except IOError as e: + logging.warn('Error IO: '+format(e)) except shutil.Error as e: - # this is ugly, but shutil does not provide more - # finely individuated errors - if str(e).endswith("already exists"): - continue - else: - raise - - # remove mail from source locations only after all copies are finished - for fname in set(to_delete_fnames): - os.remove(fname) - - # update notmuch database - if not self.dry_run: - if moved: - logging.info("updating database") - self.__update_db(maildir) - else: - logging.info("Would update database") - + logging.warn('Error shutil: '+format(e)) + continue + except OSError as e: + logging.warn('Error OS: '+format(e)) + continue + except notmuch.NotmuchError as e: + logging.warn('Error notmuch: '+format(e)) + continue + # Return the moved flag + return moved # # private: @@ -117,7 +138,6 @@ def __update_db(self, maildir): "after syncing maildir '{}': {}".format(maildir, err)) raise SystemExit - def __log_move_action(self, message, source, destination, dry_run): ''' Report which mails have been identified for moving. @@ -132,4 +152,3 @@ def __log_move_action(self, message, source, destination, dry_run): logging.log(level, " {}".format(get_message_summary(message).encode('utf8'))) logging.log(level, "from '{}' to '{}'".format(source, destination)) #logging.debug("rule: '{}' in [{}]".format(tag, message.get_tags())) - From 25606aa31858a7e3788f007c4cc98adea7256e9b Mon Sep 17 00:00:00 2001 From: Guillaume Seren Date: Mon, 2 Apr 2018 19:03:04 +0200 Subject: [PATCH 2/2] Refactor moving mail into its own function --- afew/MailMover.py | 111 ++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/afew/MailMover.py b/afew/MailMover.py index bc8a3607..6d88c210 100644 --- a/afew/MailMover.py +++ b/afew/MailMover.py @@ -51,16 +51,13 @@ def get_new_name(self, fname, destination): basename = str(uuid.uuid1()) + flagpart return os.path.join(destination, submaildir, basename) + """ Move mails in folder maildir according to the given rules. """ def move(self, maildir, rules): ''' Move mails in folder maildir according to the given rules. ''' # identify and move messages logging.info("checking mails in '{}'".format(maildir)) - # Open DB R/W if are not in dry-run - if not self.dry_run: - self.db = notmuch.Database(self.db_path, - mode=notmuch.Database.MODE.READ_WRITE) moved = False for query in rules.keys(): destination = '{}/{}/'.format(self.db_path, rules[query]) @@ -72,57 +69,77 @@ def move(self, maildir, rules): # a single message (identified by Message-ID) can be in several # places; only touch the one(s) that exists in this maildir all_message_fnames = message.get_filenames() - to_move_fnames = [name for name in all_message_fnames - if maildir in name] + to_move_fnames = [ + name for name in all_message_fnames + if maildir in name] if not to_move_fnames: continue - moved = True - self.__log_move_action(message, maildir, rules[query], - self.dry_run) for fname in to_move_fnames: if self.dry_run: continue - try: - logging.info("Copy original mail: "+fname) - logging.info("Path of mail: "+os.path.dirname(fname)) - logging.info("Copy to: "+destination) - dbpath = self.db.get_path() - logging.info("Path of DB: "+dbpath) - relpath=os.path.relpath(os.path.dirname(destination), dbpath) - filename = os.path.basename(fname) - logging.info("filename: "+filename) - logging.info("Relative path DB -> file: " + relpath) - # 1 COPY - shutil.copy2(fname, - self.get_new_name(fname, - destination)) - dest_file_rel = dbpath+'/'+relpath+'/'+filename - logging.info("Add new copy to the DB: "+dest_file_rel) - # OPEN DB R/W - logging.info("Open DB R/W") - # print vars(self.db) - # 2 Add NEW MESSAGE TO DB - moved_message = notmuch.Database.add_message(self.db, dest_file_rel) - logging.info("Delete original mail file: "+fname) - os.remove(fname) - # 3 REMOVE ORIGINAL - logging.info("Delete the original mail in DB") - # 4 REMOVE ORIGINAL FROM DB - notmuch.Database.remove_message(self.db, fname) - except IOError as e: - logging.warn('Error IO: '+format(e)) - except shutil.Error as e: - logging.warn('Error shutil: '+format(e)) - continue - except OSError as e: - logging.warn('Error OS: '+format(e)) - continue - except notmuch.NotmuchError as e: - logging.warn('Error notmuch: '+format(e)) - continue + else: + moved = self.moveMail(fname, destination, True) # Return the moved flag return moved + """ Action of moving a mail and upgrade the DB """ + def moveMail(self, mailFrom, mailTo, upgradeDatabase): + output = False + """ We need to check that we are not moving a file to itself """ + if mailFrom == mailTo: + logging.error("Can not move mail to itself: {} -> {}".format( + mailFrom, + mailTo)) + raise SystemExit + else: + try: + " mailFrom must be a file " + if not os.path.isfile(mailFrom): + logging.error("mailFrom not a file: {}".format(mailFrom)) + raise SystemExit + " mailTo must be a directory " + if not os.path.isdir(mailTo): + logging.error("mailTo not a dir: {}".format(mailTo)) + raise SystemExit + filename = os.path.basename(mailFrom) + logging.info("Copy original mail: {}".format(filename)) + logging.info("{} -> {}".format( + os.path.dirname(mailFrom), + os.path.dirname(mailTo))) + # 1 COPY + shutil.copy2(mailFrom, mailTo) + mailToFile = os.path.join(mailTo, filename) + if upgradeDatabase: + self.db = notmuch.Database(self.db_path, + mode=notmuch.Database.MODE.READ_WRITE) + logging.info("Add new copy to the DB: {}".format(mailToFile)) + notmuch.Database.add_message(self.db, mailToFile) + self.db = notmuch.Database(self.db_path) + logging.info("Delete original file: {}".format(mailFrom)) + os.remove(mailFrom) + # 3 REMOVE ORIGINAL + if upgradeDatabase: + self.db = notmuch.Database(self.db_path, + mode=notmuch.Database.MODE.READ_WRITE) + logging.info("Cleanup the original mail in DB") + # 4 REMOVE ORIGINAL FROM DB + notmuch.Database.remove_message(self.db, mailFrom) + self.db = notmuch.Database(self.db_path) + output = True + except IOError as e: + logging.error("Error IO: {}".format(e)) + raise SystemExit + except shutil.Error as e: + logging.error("shutil: {}".format(e)) + raise SystemExit + except OSError as e: + logging.error("OS: {}".format(e)) + raise SystemExit + except notmuch.NotmuchError as e: + logging.error("notmuch: {}".format(e.status)) + raise SystemExit + return output + # # private: #