afew - afew Documentation
afew is an initial tagging script for notmuch mail:
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 or in the notmuch
post-new hook.
It can do basic thing such as adding tags based on email headers or maildir
folders, handling killed threads and spam.
fyi: afew plays nicely with
alot, a GUI for notmuch mail ;)
Contents:
Ultimately afew is a command line tool. You have to specify an action, and
whether to act on all messages, or only on new messages. The actions you can
choose from are:
- tag
- run the tag filters. See Initial tagging.
- watch
- continuously monitor the mailbox for new files
- move-mails
- move mail files between maildir folders
Basic tagging stuff requires no configuration, just run
$ afew --tag --new
# or to tag *all* messages
$ afew --tag --all
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"
There is a lot more to say about general filter
Configuration and the
different
Filters provided by afew.
Adding
--dry-run to any
--tag or
--sync-tags action
prevents modification of the notmuch db. Add some
-vv goodness to see
some action.
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.
Note that in move mode, afew calls
notmuch new after moving mails around.
You can use
afew -m --notmuch-args=--no-hooks in a pre-new notmuch hook
to avoid loops.
For information on how to configure rules for move mode, what you can do with it
and what you can't, please refer to
Move Mode.
The full set of options is:
$ afew --help
Usage: afew [options] [--] [query]
Options:
-h, --help show this help message and exit
Actions:
Please specify exactly one action.
-t, --tag run the tag filters
-w, --watch continuously monitor the mailbox for new files
-m, --move-mails move mail files between maildir folders
Query modifiers:
Please specify either --all or --new or a query string.
-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
Customization of tag filters takes place in afew's config file in
~/.config/afew/config.
afew tries to adapt to the new tag that notmuch sets on new email, but has
mostly been developed and used against the
new tag. To use that, make
sure that
~/.notmuch-config contains:
afew reads the notmuch database location from notmuch config. When no database
path is set in notmuch config, afew uses the
MAILDIR environment
variable when set, or
$HOME/mail as a fallback, like notmuch CLI does.
If a relative path is provided, afew prepends
$HOME/ to the path in the
same manner as notmuch, which was introduced in version 0.28 of notmuch.
You can modify filters, and define your own versions of the base Filter that
allow you to tag messages in a similar way to the
notmuch tag command,
using the config file. The default config file is:
[SpamFilter]
[KillThreadsFilter]
[ListMailsFilter]
[ArchiveSentMailsFilter]
sent_tag = ''
[InboxFilter]
See the
Filters page for the details of those filters and the custom
arguments they accept.
You can add filters based on the base filter as well. These can be customised by
specifying settings beneath them. The standard settings, which apply to all
filters, are:
- message
- text that will be displayed while running this filter if
the verbosity is high enough.
- query
- the query to use against the messages, specified in
standard notmuch format. Note that you don't need to specify the
new tag - afew will add that when run with the --new
flag.
- tags
- the tags to add or remove for messages that match the
query. Tags to add are preceded by a + and tags to remove are
preceded by a -. Multiple tags are separated by semicolons.
- tags_blacklist
- if the message has one of these tags, don't add tags
to it. Tags are separated by semicolons.
So to add the
deer tag to any message to or from
[email protected]
you could do:
You can also (in combination with the InboxFilter) have email skip the Inbox by
removing the new tag before you get to the InboxFilter:
[Filter.2]
query = from'[email protected]'
tags = -new;+boss
message = Message from above
Showing some sample configs is the easiest way to understand. The
notmuch
initial tagging page shows a sample config:
# immediately archive all messages from "me"
notmuch tag -new -- tag:new and from:[email protected]
# delete all messages from a spammer:
notmuch tag +deleted -- tag:new and from:[email protected]
# tag all message from notmuch mailing list
notmuch tag +notmuch -- tag:new and to:[email protected]
# finally, retag all "new" messages "inbox" and "unread"
notmuch tag +inbox +unread -new -- tag:new
The (roughly) equivalent set up in afew would be:
[ArchiveSentMailsFilter]
[Filter.1]
message = Delete all messages from spammer
query = from:[email protected]
tags = +deleted;-new
[Filter.2]
message = Tag all messages from the notmuch mailing list
query = to:[email protected]
tags = +notmuch
[InboxFilter]
Not that the queries do not generally include
tag:new because this is
implied when afew is run with the
--new flag.
The differences between them is that
- •
- the ArchiveSentMailsFilter will add tags specified by
sent_tag option (default '' means add no tags. You may want
to set it to sent), as well as archiving the email. And it will not
archive email that has been sent to one of your own addresses.
- •
- the InboxFilter does not add the unread tag. But
most mail clients will manage the unread status directly in maildir.
Here are a few more example filters from github dotfiles:
[Filter.1]
query = '[email protected]'
tags = +sicsa
message = sicsa
[Filter.2]
query = 'from:[email protected] OR from:GT Silber OR from:[email protected]'
tags = +soc;+foo
message = foosoc
[Filter.3]
query = 'folder:gmail/G+'
tags = +G+
message = gmail spam
# skip inbox
[Filter.6]
query = 'to:[email protected] AND (subject:emacs OR subject:elisp OR "(defun" OR "(setq" OR PATCH)'
tags = -new
message = notmuch emacs stuff
# Assuming the following workflow: all messages for projects or releases should be tagged
# as "project/A", "project/B" respectively "release/1.0.1" or "release/1.2.0".
#
# In most cases replies to messages retain their context: the project, the release(s), ..
#
# The following config will propagate all project/... or release/... tags from a thread
# to all new messages.
[PropagateTagsByRegexInThreadFilter.1]
propagate_tags = project/.*
# do not tag spam
filter = not is:spam
[PropagateTagsByRegexInThreadFilter.2]
propagate_tags = release/.*
The default filter set (if you don't specify anything in the config) is:
[SpamFilter]
[KillThreadsFilter]
[ListMailsFilter]
[ArchiveSentMailsFilter]
[InboxFilter]
The standard filter
Configuration can be applied to these filters as
well. Though note that most of the filters below set their own value for
message, query and/or tags, and some ignore some of the standard settings.
It extends
SentMailsFilter with the following feature:
- •
- Emails filtered by this filter have the new tag
removed, so will not have the inbox tag added by the
InboxFilter.
This filter verifies DKIM signatures of E-Mails with DKIM header, and adds
dkin-ok or
dkin-fail tags.
DMARC reports usually come in ZIP files. To check the report you have to unpack
and search thru XML document which is very tedious. This filter tags the
message as follows:
if there's any SPF failure in any attachment, tag the message with
"dmarc-spf-fail" tag, otherwise tag with "dmarc-spf-ok"
if there's any DKIM failure in any attachment, tag the message with
"dmarc-dkim-fail" tag, otherwise tag with "dmarc-dkim-ok"
For each email, it looks at all folders it is in, and uses the path and filename
as a tag, for the email. So if you have a procmail or sieve set up that puts
emails in folders for you, this might be useful.
- •
- folder_explicit_list = <folder list>
- •
- Tag mails with tag in <folder list> only. <folder
list> is a space separated list, not enclosed in quotes or any other
way.
- •
- Empty list means all folders (of course blacklist still
applies).
- •
- The default is empty list.
- •
- You may use it e.g. to set tags only for specific folders
like 'Sent'.
- •
- folder_blacklist = <folder list>
- •
- Never tag mails with tag in <folder list>. <folder
list> is a space separated list, not enclosed in quotes or any other
way.
- •
- The default is to blacklist no folders.
- •
- You may use it e.g. to avoid mails being tagged as 'INBOX'
when there is the more standard 'inbox' tag.
- •
- folder_transforms = <transformation rules>
- •
- Transform folder names according to the specified rules
before tagging mails. <transformation rules> is a space separated
list consisting of 'folder:tag' style pairs. The colon separates the name
of the folder to be transformed from the tag it is to be transformed
into.
- •
- The default is to transform to folder names.
- •
- You may use the rules e.g. to transform the name of your
'Junk' folder into your 'spam' tag or fix capitalization of your draft and
sent folder:
folder_transforms = Junk:spam Drafts:draft Sent:sent
- •
- folder_lowercases = true
- •
- Use lowercase tags for all folder names
- •
- maildir_separator = <sep>
- •
- Use <sep> to split your maildir hierarchy into
individual tags.
- •
- The default is to split on '.'
- •
- If your maildir hierarchy is represented in the filesystem
as collapsed dirs, <sep> is used to split it again before applying
tags. If your maildir looks like this:
[...]
/path/to/maildir/devel.afew/[cur|new|tmp]/...
/path/to/maildir/devel.alot/[cur|new|tmp]/...
/path/to/maildir/devel.notmuch/[cur|new|tmp]/...
[...]
the mails in your afew folder will be tagged with 'devel' and 'afew'.
If instead your hierarchy is split by a more conventional '/' or any other
divider
[...]
/path/to/maildir/devel/afew/[cur|new|tmp]/...
/path/to/maildir/devel/alot/[cur|new|tmp]/...
/path/to/maildir/devel/notmuch/[cur|new|tmp]/...
[...]
you need to configure that divider to have your mails properly tagged:
This filter adds tags to a message if the named header matches the regular
expression given. The tags can be set, or based on the match. The settings you
can use are:
- •
- header = <header_name>
- •
- pattern = <regex_pattern>
- •
- tags = <tag_list>
If you surround a tag with
{} then it will be replaced with the named
match.
Some examples are:
[HeaderMatchingFilter.1]
header = X-Spam-Flag
pattern = YES
tags = +spam
[HeaderMatchingFilter.2]
header = List-Id
pattern = <(?P<list_id>.*)>
tags = +lists;+{list_id}
[HeaderMatchingFilter.3]
header = X-Redmine-Project
pattern = (?P<project>.*)
tags = +redmine;+{project}
SpamFilter and ListMailsFilter are implemented using HeaderMatchingFilter, and
are only slightly more complicated than the above examples.
This removes the
new tag, and adds the
inbox tag, to any message
that isn't killed or spam. (The new tags are set in your notmuch config, and
default to just
new.)
If the new message has been added to a thread that has already been tagged
killed then add the
killed tag to this message. This allows for
ignoring all replies to a particular thread.
This filter looks for the
List-Id header, and if it finds it, adds a tag
lists and a tag named
lists/<list-id>.
Add filter tagging mail sent directly to any of addresses defined in Notmuch
config file:
primary_email or
other_email. Default tag is
to-me and can be customized with
me_tag option.
The settings you can use are:
- •
- Add <tag> to all mails sent from one of your
configured mail addresses, and not to any of your
addresses.
- •
- The default is to add no tag, so you need to specify
something.
- •
- You may e.g. use it to tag all mails sent by you as 'sent'.
This may make special sense in conjunction with a mail client that is able
to not only search for threads but individual mails as well.
- •
- to_transforms = <transformation rules>
- •
- Transform To/Cc/Bcc e-mail addresses
to tags according to the specified rules. <transformation rules> is
a space separated list consisting of 'user_part@domain_part:tags'
style pairs. The colon separates the e-mail address to be transformed from
tags it is to be transformed into. ':tags' is optional and if empty,
'user_part' is used as tag. 'tags' can be a single tag or semi-colon
separated list of tags.
- •
- It can be used for example to easily tag posts sent to
mailing lists which at this stage don't have List-Id field.
The settings you can use are:
- •
- Add <tag> to all mails recognized as spam.
- •
- The default is 'spam'.
- •
- You may use it to tag your spam as 'junk', 'scum' or
whatever suits your mood. Note that only a single tag is supported
here.
Email will be considered spam if the header
X-Spam-Flag is present.
To customize these filters, there are basically two different possibilities:
Let's say you like the SpamFilter, but it is way too polite
- 1.
- Create an filter object and customize it
[SpamFilter.0] # note the index
message = meh
The index is required if you want to create a new SpamFilter
in
addition to the default one. If you need just one customized
SpamFilter, you can drop the index and customize the default instance.
[ShitFilter(SpamFilter)]
message = I hatez teh spam!
and create an object or two
[ShitFilter.0]
[ShitFilter.1]
message = Me hatez it too.
You can provide your own filter implementations too. You have to register your
filters via entry points. See the afew setup.py for examples on how to
register your filters. To add your filters, you just need to install your
package in the context of the afew application.
Here is a full sample configuration for move mode:
[MailMover]
folders = INBOX Junk
rename = False
max_age = 15
# rules
INBOX = 'tag:spam':Junk 'NOT tag:inbox':Archive
Junk = 'NOT tag:spam AND tag:inbox':INBOX 'NOT tag:spam':Archive
Below we explain what each bit of this means.
First you need to specify which folders should be checked for mails that are to
be moved (as a whitespace separated list). Folder names containing whitespace
need to be quoted:
folders = INBOX Junk "Sent Mail"
Then you have to specify rules that define move actions of the form
Every mail in the
<src> folder that matches a
<qry>
will be moved into the
<dst> folder associated with that query. A
message that matches multiple queries will be copied to multiple destinations.
You can bind as many rules to a maildir folder as you deem necessary. Just add
them as elements of a (whitespace separated) list.
Please note, though, that you need to specify at least one rule for every folder
given by the
folders option and at least one folder to check in order
to use the move mode.
will bind one rule to the maildir folder
INBOX that states that all mails
in said folder that carry (potentially among others) the tag
spam are
to be moved into the folder
Junk.
With
<qry> being an arbitrary notmuch query, you have the power to
construct arbitrarily flexible rules. You can check for the absence of tags
and look out for combinations of attributes:
Junk = 'NOT tag:spam AND tag:inbox':INBOX 'NOT tag:spam':Archive
The above rules will move all mails in
Junk that don't have the
spam tag but do have an
inbox tag into the directory
INBOX. All other mails not tagged with
spam will be moved into
Archive.
You can limit the age of mails you want to move by setting the
max_age
option in the configuration section. By providing
afew will only check mails at most 15 days old.
Set this option if you are using the
mbsync IMAP syncing tool.
mbsync adds a unique identifier to files' names when it syncs them. If
the
rename option is not set, moving files can cause UID conflicts and
prevent
mbsync from syncing with error messages such as "Maildir
error: duplicate UID 1234" or "UID 567 is beyond highest assigned
UID 89".
When the option is set, afew will rename files while moving them, removing the
UID but preserving other
mbsync information. This allows
mbsync
to assign a new UID to the file and avoid UID conflicts.
If you are using
offlineimap, you can safely ignore this option.
(1) Rules don't manipulate tags.
INBOX = 'NOT tag:inbox':Archive
Junk = 'NOT tag:spam':INBOX
The above combination of rules might prove tricky, since you might expect
de-spammed mails to end up in
INBOX. But since the
Junk rule
will
not add an
inbox tag, the next run in move mode might very
well move the matching mails into
Archive.
Then again, if you remove the
spam tag and do not set an
inbox
tag, how would you come to expect the mail would end up in your INBOX folder
after moving it? ;)
(2) There is no 1:1 mapping between folders and tags. And that's a
feature. If you tag a mail with two tags and there is a rule for each of them,
both rules will apply. Your mail will be copied into two destination folders,
then removed from its original location.
You can put python files in
~/.config/afew/ and they will be imported by
afew. If you use that python file to define a
Filter class and use the
register_filter decorator then you can refer to it in your filter
configuration.
So an example small filter you could add might be:
from afew.filters.BaseFilter import Filter
from afew.FilterRegistry import register_filter
PROJECT_MAPPING = {
'fabric': 'deployment',
'oldname': 'new-name',
}
@register_filter
class RedmineFilter(Filter):
message = 'Create tag based on redmine project'
query = 'NOT tag:redmine'
def handle_message(self, message):
project = message.get_header('X-Redmine-Project')
if project in PROJECT_MAPPING:
project = PROJECT_MAPPING[project]
self.add_tags(message, 'redmine', project)
We have defined the
message and
query class variables that are
used by the parent class
Filter. The
message is printed when
running with verbose flags. The
query is used to select messages to run
against - here we ensure we don't bother looking at messages we've already
looked at.
The
handle_message() method is the key one to implement. This will be
called for each message that matches the query. The argument is a
notmuch
message object and the key methods used by the afew filters are
get_header(),
get_filename() and
get_thread().
Of the methods inherited from the
Filter class the key ones are
add_tags() and
remove_tags(), but read about the
Implementation or just read the source code to get your own ideas.
Once you've defined your filter, you can add it to your config like any other
filter:
The design of the database manager was inspired by alots database manager
alot.db.DBManager.
- class afew.Database.Database
- Convenience wrapper around notmuch.
- add_message(path, sync_maildir_flags=False,
new_mail_handler=None)
- Adds the given message to the notmuch index.
- Parameters
- •
-
path (str) -- path to the message
- •
-
sync_maildir_flags (bool) -- if True
notmuch converts the standard maildir flags to tags
- •
-
new_mail_handler (a function that is called with a
notmuch.Message object as its only argument) -- callback for new
messages
- Raises
-
notmuch.NotmuchError if adding the message
fails
- Returns
- a notmuch.Message object
- close()
- Closes the notmuch database if it has been opened.
- do_query(query)
- Executes a notmuch query.
- Parameters
-
query (str) -- the query to execute
- Returns
- the query result
- Return type
- notmuch.Query
- get_messages(query, full_thread=False)
- Get all messages mathing the given query.
- Parameters
- •
-
query (str) -- the query to execute using
Database.do_query()
- •
-
full_thread (bool) -- return all messages
from mathing threads
- Returns
- an iterator over notmuch.Message objects
- remove_message(path)
- Remove the given message from the notmuch index.
- Parameters
-
path (str) -- path to the message
- walk_replies(message)
- Returns all replies to the given message.
- Parameters
-
message (notmuch.Message) -- the message to
start from
- Returns
- an iterator over notmuch.Message objects
- walk_thread(thread)
- Returns all messages in the given thread.
- Parameters
-
thread (notmuch.Thread) -- the tread you are
interested in
- Returns
- an iterator over notmuch.Message objects
- class afew.filters.BaseFilter.Filter(database,
**kwargs)
- flush_changes()
- (Re)Initializes the data structures that hold the enqueued
changes to the notmuch database.
- •
- Index
- •
- Module Index
- •
- Search Page
Justus Winter
afewmail project