[Python-modules-commits] [afew] 01/02: New upstream version 0.0+git2016.02.29.b19a88f
Free Ekanayaka
freee at moszumanska.debian.org
Fri Dec 23 10:19:31 UTC 2016
This is an automated email from the git hooks/post-receive script.
freee pushed a commit to branch master
in repository afew.
commit 3eff53f1a1badccd800ced58e539eebe86ab212b
Author: Free Ekanayaka <freee at debian.org>
Date: Fri Dec 23 00:02:23 2016 +0000
New upstream version 0.0+git2016.02.29.b19a88f
---
.gitignore | 7 +
NEWS.md | 23 +++
README.md | 256 ++++++++++++++++++++++++++++++++
afew/DBACL.py | 112 ++++++++++++++
afew/Database.py | 199 +++++++++++++++++++++++++
afew/FilterRegistry.py | 77 ++++++++++
afew/MailMover.py | 139 ++++++++++++++++++
afew/NotmuchSettings.py | 36 +++++
afew/Settings.py | 109 ++++++++++++++
afew/__init__.py | 18 +++
afew/commands.py | 205 ++++++++++++++++++++++++++
afew/configparser.py | 39 +++++
afew/defaults/afew.config | 50 +++++++
afew/files.py | 209 ++++++++++++++++++++++++++
afew/filters/ArchiveSentMailsFilter.py | 31 ++++
afew/filters/BaseFilter.py | 126 ++++++++++++++++
afew/filters/ClassifyingFilter.py | 41 ++++++
afew/filters/FolderNameFilter.py | 81 +++++++++++
afew/filters/HeaderMatchingFilter.py | 47 ++++++
afew/filters/InboxFilter.py | 40 +++++
afew/filters/KillThreadsFilter.py | 31 ++++
afew/filters/ListMailsFilter.py | 29 ++++
afew/filters/SentMailsFilter.py | 96 ++++++++++++
afew/filters/SpamFilter.py | 33 +++++
afew/filters/__init__.py | 28 ++++
afew/main.py | 86 +++++++++++
afew/tests/__init__.py | 15 ++
afew/tests/test_settings.py | 58 ++++++++
afew/utils.py | 138 ++++++++++++++++++
docs/Makefile | 130 +++++++++++++++++
docs/source/_static/.keep | 0
docs/source/_templates/.keep | 0
docs/source/classification.rst | 57 ++++++++
docs/source/commandline.rst | 141 ++++++++++++++++++
docs/source/conf.py | 259 +++++++++++++++++++++++++++++++++
docs/source/configuration.rst | 160 ++++++++++++++++++++
docs/source/extending.rst | 51 +++++++
docs/source/filters.rst | 238 ++++++++++++++++++++++++++++++
docs/source/implementation.rst | 45 ++++++
docs/source/index.rst | 43 ++++++
docs/source/installation.rst | 35 +++++
docs/source/move_mode.rst | 103 +++++++++++++
docs/source/quickstart.rst | 84 +++++++++++
setup.py | 54 +++++++
44 files changed, 3759 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9869866
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.py[co]
+/dist
+/build
+bin/
+include/
+lib/
+/afew.egg-info
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 0000000..9fb7308
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,23 @@
+afew x.x (xxxx-xx-xx)
+=====================
+
+Filter behaviour change
+
+ As of commit d98a0cd0d1f37ee64d03be75e75556cff9f32c29, the ListMailsFilter
+ does not add a tag named `list-id `anymore, but a new one called
+ `lists/<list-id>`.
+
+afew 0.1pre (2012-02-10)
+========================
+
+Configuration format change
+
+ Previously the values for configuration entries with the key `tags`
+ were interpreted as a whitespace delimited list of strings. As of
+ commit e4ec3ced16cc90c3e9c738630bf0151699c4c087 those entries are
+ split at semicolons (';').
+
+ This changes the semantic of the configuration file and affects
+ everyone who uses filter rules that set or remove more than one tag
+ at once. Please inspect your configuration files and adjust them if
+ necessary.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4d1dd6e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,256 @@
+About
+=====
+
+afew is an initial tagging script for notmuch mail:
+
+* http://notmuchmail.org/
+* http://notmuchmail.org/initial_tagging/
+
+Its basic task is to provide automatic tagging each time new mail is registered
+with notmuch. In a classic setup, you might call it after 'notmuch new' in an
+offlineimap post sync hook.
+
+In addition to more elementary features such as adding tags based on email
+headers or maildir folders, handling killed threads and spam, it can do some
+heavy magic in order to /learn/ how to initially tag your mails based on their
+content.
+
+In move mode, afew will move mails between maildir folders according to
+configurable rules that can contain arbitrary notmuch queries to match against
+any searchable attributes.
+
+fyi: afew plays nicely with alot, a GUI for notmuch mail ;)
+
+* https://github.com/pazz/alot
+
+
+Current NEWS
+============
+
+afew is quite young, so expect a few user visible API or configuration
+format changes, though I'll try to minimize such disruptive events.
+
+Please keep an eye on NEWS.md for important news.
+
+
+Features
+========
+
+* text classification, magic tags aka the mailing list without server
+* spam handling (flush all tags, add spam)
+* killed thread handling
+* tags posts to lists with `lists`, `$list-id`
+* autoarchives mails sent from you
+* catchall -> remove `new`, add `inbox`
+* can operate on new messages [default], `--all` messages or on custom
+ query results
+* can move mails based on arbitrary notmuch queries, so your sorting
+ may show on your traditional mail client (well, almost ;))
+* has a `--dry-run` mode for safe testing
+* works with python 2.7, 3.1 and 3.2
+
+
+Installation
+============
+
+You'll need dbacl for the text classification:
+
+ # aptitude install dbacl
+
+And I'd like to suggest to install afew as your unprivileged user.
+If you do, make sure `~/.local/bin` is in your path.
+
+ $ python setup.py install --prefix=~/.local
+ $ mkdir -p ~/.config/afew ~/.local/share/afew/categories
+
+
+Configuration
+=============
+
+Make sure that `~/.notmuch-config` reads:
+
+```
+[new]
+tags=new
+```
+
+Put a list of filters into `~/.config/afew/config`:
+
+```
+# This is the default filter chain
+[SpamFilter]
+[ClassifyingFilter]
+[KillThreadsFilter]
+[ListMailsFilter]
+[ArchiveSentMailsFilter]
+[InboxFilter]
+```
+
+And configure rules to sort mails on your disk, if you want:
+
+```
+[MailMover]
+folders = INBOX Junk
+max_age = 15
+
+# rules
+INBOX = 'tag:spam':Junk 'NOT tag:inbox':Archive
+Junk = 'NOT tag:spam AND tag:inbox':INBOX 'NOT tag:spam':Archive
+```
+
+
+Commandline help
+================
+
+```
+$ afew --help
+Usage: afew [options] [--] [query]
+
+Options:
+ -h, --help show this help message and exit
+
+ Actions:
+ Please specify exactly one action (both update actions can be
+ specified simultaniously).
+
+ -t, --tag run the tag filters
+ -l LEARN, --learn=LEARN
+ train the category with the messages matching the
+ given query
+ -u, --update update the categories [requires no query]
+ -U, --update-reference
+ update the reference category (takes quite some time)
+ [requires no query]
+ -c, --classify classify each message matching the given query (to
+ test the trained categories)
+ -m, --move-mails move mail files between maildir folders
+
+ Query modifiers:
+ Please specify either --all or --new or a query string. The default
+ query for the update actions is a random selection of
+ REFERENCE_SET_SIZE mails from the last REFERENCE_SET_TIMEFRAME days.
+
+ -a, --all operate on all messages
+ -n, --new operate on all new messages
+
+ General options:
+ -C NOTMUCH_CONFIG, --notmuch-config=NOTMUCH_CONFIG
+ path to the notmuch configuration file [default:
+ $NOTMUCH_CONFIG or ~/.notmuch-config]
+ -e ENABLE_FILTERS, --enable-filters=ENABLE_FILTERS
+ filter classes to use, separated by ',' [default:
+ filters specified in afew's config]
+ -d, --dry-run don't change the db [default: False]
+ -R REFERENCE_SET_SIZE, --reference-set-size=REFERENCE_SET_SIZE
+ size of the reference set [default: 1000]
+ -T DAYS, --reference-set-timeframe=DAYS
+ do not use mails older than DAYS days [default: 30]
+ -v, --verbose be more verbose, can be given multiple times
+```
+
+
+Boring stuff
+============
+
+Simulation
+----------
+Adding `--dry-run` to any `--tag` or `--sync-tags` action prevents
+modification of the notmuch db. Add some `-vv` goodness to see some
+action.
+
+
+Initial tagging
+---------------
+Basic tagging stuff requires no configuration, just run
+
+ $ afew --tag --new
+
+To do this automatically you can add the following hook into your
+~/.offlineimaprc:
+
+ postsynchook = ionice -c 3 chrt --idle 0 /bin/sh -c "notmuch new && afew --tag --new"
+
+
+Tag filters
+-----------
+Tag filters are plugin-like modules that encapsulate tagging
+functionality. There is a filter that handles the archiving of mails
+you sent, one that handles spam, one for killed threads, one for
+mailing list magic...
+
+The tag filter concept allows you to easily extend afew's tagging
+abilities by writing your own filters. Take a look at the default
+configuration file (`afew/defaults/afew.config`) for a list of
+available filters and how to enable filters and create customized
+filter types.
+
+
+Move mode
+---------
+
+To invoke afew in move mode, provide the `--move-mails` option on the
+command line. Move mode will respect `--dry-run`, so throw in
+`--verbose` and watch what effects a real run would have.
+
+In move mode, afew will check all mails (or only recent ones) in the
+configured maildir folders, deciding whether they should be moved to
+another folder.
+
+The decision is based on rules defined in your config file. A rule is
+bound to a source folder and specifies a target folder into which a
+mail will be moved that is matched by an associated query.
+
+This way you will be able to transfer your sorting principles roughly
+to the classic folder based maildir structure understood by your
+traditional mail server. Tag your mails with notmuch, call afew
+`--move-mails` in an offlineimap presynchook and enjoy a clean inbox
+in your webinterface/GUI-client at work.
+
+For information on how to configure rules for move mode, what you can
+do with it and what you can't, please refer to `docs/move_mode`.
+
+
+The real deal
+=============
+
+Let's train on an existing tag `spam`:
+
+ $ afew --learn spam -- tag:spam
+
+Let's build the reference category. This is important to reduce the
+false positive rate. This may take a while...
+
+ $ afew --update-reference
+
+And now let's create a new tag from an arbitrary query result:
+
+ $ afew -vv --learn sourceforge -- sourceforge
+
+Let's see how good the classification is:
+
+ $ afew --classify -- tag:inbox and not tag:killed
+ Sergio López <slpml at sinrega.org> (2011-10-08) (bug-hurd inbox lists unread) --> no match
+ Patrick Totzke <reply+i-1840934-9a702d09342dca2b120126b26b008d0deea1731e at reply.github.com> (2011-10-08) (alot inbox lists) --> alot
+ [...]
+
+As soon as you trained some categories, afew will automatically
+tag your new mails using the classifier. If you want to disable this
+feature, either use the `--enable-filters` option to override the default
+set of filters or remove the files in your afew state dir:
+
+ $ ls ~/.local/share/afew/categories
+ alot juggling reference_category sourceforge spam
+
+You need to update the category files periodically. I'd suggest to run
+
+ $ afew --update
+
+on a weekly and
+
+ $ afew --update-reference
+
+on a monthly basis.
+
+
+
+Have fun :)
diff --git a/afew/DBACL.py b/afew/DBACL.py
new file mode 100644
index 0000000..0fb82b8
--- /dev/null
+++ b/afew/DBACL.py
@@ -0,0 +1,112 @@
+# coding=utf-8
+from __future__ import print_function, absolute_import, unicode_literals
+
+#
+# Copyright (c) Justus Winter <4winter at informatik.uni-hamburg.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import os
+import glob
+import logging
+import functools
+import subprocess
+
+class ClassificationError(Exception): pass
+class BackendError(ClassificationError): pass
+
+default_db_path = os.path.join(os.environ.get('XDG_DATA_HOME',
+ os.path.expanduser('~/.local/share')),
+ 'afew', 'categories')
+
+class Classifier(object):
+ reference_category = 'reference_category'
+
+ def __init__(self, categories, database_directory = default_db_path):
+ self.categories = set(categories)
+ self.database_directory = database_directory
+
+ def learn(self, category, texts):
+ pass
+
+ def classify(self, text):
+ pass
+
+class DBACL(Classifier):
+ def __init__(self, database_directory = default_db_path):
+ categories = glob.glob1(database_directory, '*')
+ super(DBACL, self).__init__(categories, database_directory)
+
+ sane_environ = {
+ key: value
+ for key, value in os.environ.items()
+ if not (
+ key.startswith('LC_') or
+ key == 'LANG' or
+ key == 'LANGUAGE'
+ )
+ }
+
+ def _call_dbacl(self, args, **kwargs):
+ command_line = ['dbacl', '-T', 'email'] + args
+ logging.debug('executing %r' % command_line)
+ return subprocess.Popen(
+ command_line,
+ shell = False,
+ stdin = subprocess.PIPE,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.PIPE,
+ env = self.sane_environ,
+ **kwargs
+ )
+
+ def get_category_path(self, category):
+ return os.path.join(self.database_directory, category.replace('/', '_'))
+
+ def learn(self, category, texts):
+ process = self._call_dbacl(['-l', self.get_category_path(category)])
+
+ for text in texts:
+ process.stdin.write((text + '\n').encode('utf-8'))
+
+ process.stdin.close()
+ process.wait()
+
+ if process.returncode != 0:
+ raise BackendError('dbacl learning failed:\n%s' % process.stderr.read())
+
+ def classify(self, text):
+ if not self.categories:
+ raise ClassificationError('No categories defined')
+
+ categories = functools.reduce(list.__add__, [
+ ['-c', self.get_category_path(category)]
+ for category in self.categories
+ ], [])
+
+ process = self._call_dbacl(categories + ['-n'])
+ stdout, stderr = process.communicate(text.encode('utf-8'))
+
+ if len(stderr) == 0:
+ result = stdout.split()
+ scores = list()
+ while result:
+ category = result.pop(0).decode('utf-8', 'replace')
+ score = float(result.pop(0))
+ scores.append((category, score))
+ scores.sort(key = lambda category_score: category_score[1])
+ else:
+ raise BackendError('dbacl classification failed:\n%s' % stderr)
+
+ return scores
diff --git a/afew/Database.py b/afew/Database.py
new file mode 100644
index 0000000..4b38f0c
--- /dev/null
+++ b/afew/Database.py
@@ -0,0 +1,199 @@
+# coding=utf-8
+from __future__ import print_function, absolute_import, unicode_literals
+
+#
+# Copyright (c) Justus Winter <4winter at informatik.uni-hamburg.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import time
+import logging
+
+import notmuch
+
+from .NotmuchSettings import notmuch_settings, get_notmuch_new_tags
+from .utils import extract_mail_body
+
+class Database(object):
+ '''
+ Convenience wrapper around `notmuch`.
+ '''
+
+ def __init__(self):
+ self.db_path = notmuch_settings.get('database', 'path')
+ self.handle = None
+
+ def __enter__(self):
+ '''
+ Implements the context manager protocol.
+ '''
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ '''
+ Implements the context manager protocol.
+ '''
+ self.close()
+
+ def open(self, rw=False, retry_for=180, retry_delay=1):
+ if rw:
+ if self.handle and self.handle.mode == notmuch.Database.MODE.READ_WRITE:
+ return self.handle
+
+ start_time = time.time()
+ while True:
+ try:
+ self.handle = notmuch.Database(self.db_path,
+ mode = notmuch.Database.MODE.READ_WRITE)
+ break
+ except notmuch.NotmuchError:
+ time_left = int(retry_for - (time.time() - start_time))
+
+ if time_left <= 0:
+ raise
+
+ if time_left % 15 == 0:
+ logging.debug('Opening the database failed. Will keep trying for another {} seconds'.format(time_left))
+
+ time.sleep(retry_delay)
+ else:
+ if not self.handle:
+ self.handle = notmuch.Database(self.db_path)
+
+ return self.handle
+
+ def close(self):
+ '''
+ Closes the notmuch database if it has been opened.
+ '''
+ if self.handle:
+ self.handle.close()
+ self.handle = None
+
+ def do_query(self, query):
+ '''
+ Executes a notmuch query.
+
+ :param query: the query to execute
+ :type query: str
+ :returns: the query result
+ :rtype: :class:`notmuch.Query`
+ '''
+ logging.debug('Executing query %r' % query)
+ return notmuch.Query(self.open(), query)
+
+ def get_messages(self, query, full_thread = False):
+ '''
+ Get all messages mathing the given query.
+
+ :param query: the query to execute using :func:`Database.do_query`
+ :type query: str
+ :param full_thread: return all messages from mathing threads
+ :type full_thread: bool
+ :returns: an iterator over :class:`notmuch.Message` objects
+ '''
+ if not full_thread:
+ for message in self.do_query(query).search_messages():
+ yield message
+ else:
+ for thread in self.do_query(query).search_threads():
+ for message in self.walk_thread(thread):
+ yield message
+
+
+ def mail_bodies_matching(self, *args, **kwargs):
+ '''
+ Filters each message yielded from
+ :func:`Database.get_messages` through
+ :func:`afew.utils.extract_mail_body`.
+
+ This functions accepts the same arguments as
+ :func:`Database.get_messages`.
+
+ :returns: an iterator over :class:`list` of :class:`str`
+ '''
+ query = self.get_messages(*args, **kwargs)
+ for message in query:
+ yield extract_mail_body(message)
+
+ def walk_replies(self, message):
+ '''
+ Returns all replies to the given message.
+
+ :param message: the message to start from
+ :type message: :class:`notmuch.Message`
+ :returns: an iterator over :class:`notmuch.Message` objects
+ '''
+ yield message
+
+ # TODO: bindings are *very* unpythonic here... iterator *or* None
+ # is a nono
+ replies = message.get_replies()
+ if replies != None:
+ for message in replies:
+ # TODO: yield from
+ for message in self.walk_replies(message):
+ yield message
+
+ def walk_thread(self, thread):
+ '''
+ Returns all messages in the given thread.
+
+ :param message: the tread you are interested in
+ :type message: :class:`notmuch.Thread`
+ :returns: an iterator over :class:`notmuch.Message` objects
+ '''
+ for message in thread.get_toplevel_messages():
+ # TODO: yield from
+ for message in self.walk_replies(message):
+ yield message
+
+ def add_message(self, path, sync_maildir_flags=False, new_mail_handler=None):
+ '''
+ Adds the given message to the notmuch index.
+
+ :param path: path to the message
+ :type path: str
+ :param sync_maildir_flags: if `True` notmuch converts the
+ standard maildir flags to tags
+ :type sync_maildir_flags: bool
+ :param new_mail_handler: callback for new messages
+ :type new_mail_handler: a function that is called with a
+ :class:`notmuch.Message` object as
+ its only argument
+ :raises: :class:`notmuch.NotmuchError` if adding the message fails
+ :returns: a :class:`notmuch.Message` object
+ '''
+ # TODO: it would be nice to update notmuchs directory index here
+ message, status = self.open(rw=True).add_message(path, sync_maildir_flags=sync_maildir_flags)
+
+ if status != notmuch.STATUS.DUPLICATE_MESSAGE_ID:
+ logging.info('Found new mail in {}'.format(path))
+
+ for tag in get_notmuch_new_tags():
+ message.add_tag(tag)
+
+ if new_mail_handler:
+ new_mail_handler(message)
+
+ return message
+
+ def remove_message(self, path):
+ '''
+ Remove the given message from the notmuch index.
+
+ :param path: path to the message
+ :type path: str
+ '''
+ self.open(rw=True).remove_message(path)
diff --git a/afew/FilterRegistry.py b/afew/FilterRegistry.py
new file mode 100644
index 0000000..ef77398
--- /dev/null
+++ b/afew/FilterRegistry.py
@@ -0,0 +1,77 @@
+# coding=utf-8
+from __future__ import print_function, absolute_import, unicode_literals
+
+#
+# Copyright (c) Justus Winter <4winter at informatik.uni-hamburg.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import pkg_resources
+
+
+RAISEIT = object()
+
+
+class FilterRegistry(object):
+ """
+ The FilterRegistry is responsible for returning
+ filters by key.
+ Filters get registered via entry points.
+ To avoid any circular dependencies, the registry loads
+ the Filters lazily
+ """
+ def __init__(self, filters):
+ self._filteriterator = filters
+
+ @property
+ def filter(self):
+ if not hasattr(self, '_filter'):
+ self._filter = {}
+ for f in self._filteriterator:
+ self._filter[f.name] = f.load()
+ return self._filter
+
+ def get(self, key, default=RAISEIT):
+ if default == RAISEIT:
+ return self.filter[key]
+ else:
+ return self.filter.get(key, default)
+
+ def __getitem__(self, key):
+ return self.get(key)
+
+ def __setitem__(self, key, value):
+ self.filter[key] = value
+
+ def __delitem__(self, key):
+ del self.filter[key]
+
+ def keys(self):
+ return self.filter.keys()
+
+ def values(self):
+ return self.filter.values()
+
+ def items(self):
+ return self.filter.items()
+
+
+all_filters = FilterRegistry(pkg_resources.iter_entry_points('afew.filter'))
+
+def register_filter (klass):
+ '''Decorator function for registering a class as a filter.'''
+
+ all_filters[klass.__name__] = klass
+ return klass
+
diff --git a/afew/MailMover.py b/afew/MailMover.py
new file mode 100644
index 0000000..faa89c7
--- /dev/null
+++ b/afew/MailMover.py
@@ -0,0 +1,139 @@
+# coding=utf-8
+
+#
+# Copyright (c) dtk <dtk at gmx.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+
+import notmuch
+import logging
+import os, shutil
+from subprocess import check_call, CalledProcessError
+
+from .Database import Database
+from .utils import get_message_summary
+from datetime import date, datetime, timedelta
+import uuid
+
+
+class MailMover(Database):
+ '''
+ Move mail files matching a given notmuch query into a target maildir folder.
+ '''
+
+
+ def __init__(self, max_age=0, rename = False, dry_run=False):
+ 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.dry_run = dry_run
+ self.rename = rename
+
+ def get_new_name(self, fname, destination):
+ if self.rename:
+ return os.path.join(
+ destination,
+ # construct a new filename, composed of a made-up ID and the flags part
+ # of the original filename.
+ str(uuid.uuid1()) + ':' + os.path.basename(fname).split(':')[-1]
+ )
+ else:
+ return destination
+
+ 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))
+ to_delete_fnames = []
+ for query in rules.keys():
+ destination = '{}/{}/cur/'.format(self.db_path, rules[query])
+ main_query = self.query.format(folder=maildir, subquery=query)
+ logging.debug("query: {}".format(main_query))
+ 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
+ all_message_fnames = message.get_filenames()
+ to_move_fnames = [name for name in all_message_fnames
+ if maildir in name]
+ if not to_move_fnames:
+ continue
+ self.__log_move_action(message, maildir, rules[query],
+ self.dry_run)
+ for fname in to_move_fnames:
+ if self.dry_run:
+ continue
+ try:
+ shutil.copy2(fname, self.get_new_name(fname, destination))
+ to_delete_fnames.append(fname)
+ 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
+ logging.info("updating database")
+ if not self.dry_run:
+ self.__update_db(maildir)
+ else:
+ logging.info("Would update database")
+
+
+ #
+ # private:
+ #
+
+ def __update_db(self, maildir):
+ '''
+ Update the database after mail files have been moved in the filesystem.
+ '''
+ try:
+ check_call(['notmuch', 'new'])
+ except CalledProcessError as err:
+ logging.error("Could not update notmuch database " \
+ "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.
+ '''
+ if not dry_run:
+ level = logging.DEBUG
+ prefix = 'moving mail'
+ else:
+ level = logging.INFO
+ prefix = 'I would move mail'
+ logging.log(level, prefix)
+ 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()))
+
diff --git a/afew/NotmuchSettings.py b/afew/NotmuchSettings.py
new file mode 100644
index 0000000..13655e0
--- /dev/null
+++ b/afew/NotmuchSettings.py
@@ -0,0 +1,36 @@
+# coding=utf-8
+from __future__ import print_function, absolute_import, unicode_literals
+
+#
+# Copyright (c) Justus Winter <4winter at informatik.uni-hamburg.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import os
+
+from .configparser import RawConfigParser
+
+notmuch_settings = RawConfigParser()
+
+def read_notmuch_settings(path = None):
+ if path == None:
+ path = os.environ.get('NOTMUCH_CONFIG', os.path.expanduser('~/.notmuch-config'))
+
+ notmuch_settings.readfp(open(path))
+
+def get_notmuch_new_tags():
+ return notmuch_settings.get_list('new', 'tags')
+
+def get_notmuch_new_query():
+ return '(%s)' % ' AND '.join('tag:%s' % tag for tag in get_notmuch_new_tags())
diff --git a/afew/Settings.py b/afew/Settings.py
new file mode 100644
index 0000000..fe2ed61
--- /dev/null
+++ b/afew/Settings.py
@@ -0,0 +1,109 @@
+# coding=utf-8
+from __future__ import print_function, absolute_import, unicode_literals
+
+#
+# Copyright (c) Justus Winter <4winter at informatik.uni-hamburg.de>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+import os
+import re
+import collections
+
+from .configparser import SafeConfigParser
+from afew.FilterRegistry import all_filters
+
+user_config_dir = os.path.join(os.environ.get('XDG_CONFIG_HOME',
+ os.path.expanduser('~/.config')),
+ 'afew')
+user_config_dir=os.path.expandvars(user_config_dir)
+
+settings = SafeConfigParser()
+# preserve the capitalization of the keys.
+settings.optionxform = str
+
+settings.readfp(open(os.path.join(os.path.dirname(__file__), 'defaults', 'afew.config')))
+settings.read(os.path.join(user_config_dir, 'config'))
+
+# All the values for keys listed here are interpreted as ;-delimited lists
+value_is_a_list = ['tags', 'tags_blacklist']
+mail_mover_section = 'MailMover'
+
+section_re = re.compile(r'^(?P<name>[a-z_][a-z0-9_]*)(\((?P<parent_class>[a-z_][a-z0-9_]*)\)|\.(?P<index>\d+))?$', re.I)
+def get_filter_chain(database):
+ filter_chain = []
... 3069 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/afew.git
More information about the Python-modules-commits
mailing list