I’m available for freelance work. Let’s talk »

Gefilte Fish: gmail filter creation

Sunday 28 March 2021

At work, to keep up with mailing lists and GitHub notifications, I had more than fifty GMail filters. It wasn’t too bad to create them by hand with the GMail UI, but I’m sure there were filters there I didn’t need any more.

But then I wanted a filter with both an if-action, and an else-action. Worse, I wanted if-A, then do this, if-B, do this, else, do that. GMail filters just aren’t constructed that way. It was going to be a pain to set them up and maintain them.

Looking around for tools, I found gmail-britta, a Ruby DSL. This was the right kind of tool for me, except I don’t write Ruby. I hadn’t found gmail-yaml-filters, but I don’t think I want to write YAML.

gmail-tools looked promising, but my work GMail account wouldn’t let me follow its authentication steps. Honestly, I often run afoul of authentication when trying to use APIs. (See Support windows bar calendar for another project I built in a strange way specifically to avoid having to figure out authentication.)

So naturally, I built my own module to do it: Gefilte Fish is a Python DSL (domain-specific language) of sorts to create GMail filters. (The name is fitting since this is the start of Passover.) Using gefilte, you write Python code to express your filters. Running your program outputs XML that you then import into GMail to create the filters.

The DSL lets you write this to make filters:

from gefilte import GefilteFish

# Make the filter-maker and use its DSL. All of the methods of GitHubFilter
# are now usable as global functions.
fish = GefilteFish()
with fish.dsl():

    # Google's spam moderation messages should never get sent to spam.
    with replyto("noreply-spamdigest@google.com"):
        never_spam()
        mark_important()

    # If the subject and body have these, label it "liked".
    with subject(exact("[Confluence]")).has(exact("liked this page")):
        label("liked")

    with from_("notifications@github.com"):
        # Skip the inbox (archive them).
        skip_inbox().label("github")

        # Delete annoying bot messages.
        with from_("renovate[bot]"):
            delete()

        # GitHub sends to synthetic addresses to provide information.
        with to("author@noreply.github.com"):
            label("mine").star()

        # Notifications from some repos are special.
        with repo("myproject/tasks") as f:
            label("todo")
            with f.elif_(repo("otherproject/something")) as f:
                label("otherproject")
                with f.else_():
                    # But everything else goes into "Code reviews".
                    label("Code reviews")

    # Some inbound addresses come to me, mark them so
    # I understand what I'm # looking at in my inbox.
    for toaddr, the_label in [
        ("info@mycompany.com", "info@"),
        ("security@mycompany.com", "security@"),
        ("con2020@mycompany.com", "con20"),
        ("con2021@mycompany.com", "con21"),
    ]:
        with to(toaddr):
            label(the_label)

print(fish.xml())

To make the DSL flow somewhat naturally, I definitely bent the rules on what is considered good Python. But it let me write succinct descriptions of the filters I want, while still having the power of a programming language.

Comments

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.