Giter VIP home page Giter VIP logo

gmailctl's Introduction

gmailctl

Go Report Card Go

This utility helps you generate and maintain Gmail filters in a declarative way. It has a Jsonnet configuration file that aims to be simpler to write and maintain than using the Gmail web interface, to categorize, label, archive and manage your inbox automatically.

Table of contents

Motivation

If you use Gmail and have to maintain (like me) a lot of filters (to apply labels, get rid of spam or categorize your emails), then you probably have (like me) a very long list of messy filters. At a certain point one of your messages got mislabled and you try to understand why. You scroll through that horrible mess of filters, you wish you could find-and-replace stuff, test the changes on your filters before applying them, refactor some filters together... in a way treat them like you treat your code!

Gmail allows one to import and export filters in XML format. This can be used to maintain them in some better way... but dear Lord, no! Not by hand! That's what most other tools do: providing some kind of DSL that generate XML filters that can be imported in your settings... by hand [this is the approach of the popular antifuchs/gmail-britta for example].

Gmail happens to have also a neat API that we can use to automate the import step as well, so to eliminate all manual, slow tasks to be done with the Gmail settings.

This project then exists to provide to your Gmail filters:

  1. Maintainability;
  2. An easy to understand, declarative, composable language;
  3. A builtin query simplifier, to keep the size of your filters down (Gmail has a limit of 1500 chars per filter);
  4. Ability to review your changes before applying them;
  5. Automatic update of the settings (no manual import) in seconds.

Install

gmailctl is written in Go and requires at least Go version 1.17. Make sure to setup your $GOPATH correctly, including the bin subdirectory in your $PATH.

go install github.com/mbrt/gmailctl/cmd/gmailctl@latest

Alternatively, if you're on macOS, you can install easily via Homebrew or Macports:

# Install with Homebrew
brew install gmailctl
# Install with Macports
sudo port install gmailctl

On Fedora Linux, you can install from the official repositories:

sudo dnf install gmailctl

You can also choose to install the snap:

sudo snap install gmailctl

If so, make sure to configure xdg-mime to open the config file with your favorite editor. For example, if you'd like to use vim:

xdg-mime default vim.desktop text/x-csrc

Once installed, run the init process:

gmailctl init

This will guide you through setting up the Gmail APIs and update your settings without leaving your command line.

Usage

asciicast

The easiest way to use gmailctl is to run gmailctl edit. This will open the local .gmailctl/config.jsonnet file in your editor. After you exit the editor the configuration is applied to Gmail. See Configuration for the configuration file format. This is the preferred way if you want to start your filters from scratch.

NOTE: It's recommended to backup your current configuration before you apply the generated one for the first time. Your current filters will be wiped and replaced with the ones specified in the config file. The diff you'll get during the first run will probably be pretty big, but from that point on, all changes should generate a small and simple to review diff.

Migrate from another solution

If you want to preserve your current filters and migrate to a more sane configuration gradually, you can try to use the download command. This will look up at your currently configured filters in Gmail and try to create a configuration file matching the current state.

NOTE: This functionality is experimental. It's recommended to download the filters and check that they correspond to the remote ones before making any changes, to avoid surprises. Also note that the configuration file will be quite ugly, as expressions won't be reconstructed properly, but it should serve as a starting point if you are migrating from other systems.

Example of usage:

# download the filters to the default configuration file
gmailctl download > ~/.gmailctl/config.jsonnet
# check that the diff is empty and no errors are present
gmailctl diff
# happy editing!
gmailctl edit

Often you'll see imported filters with the isEscaped: true marker. This tells gmailctl to not escape or quote the expression, as it might contain operators that have to be interpreted as-is by Gmail. This happens when the download command was unable to map the filter to native gmailctl expressions. It's recommended to manually port the filter to regular gmailctl operators before doing any changes, to avoid unexpected results. Example of such conversion:

{
  from: "{foo bar baz}",
  isEscaped: true,
}

Can be translated into:

{
  or: [
    {from: "foo"},
    {from: "bar"},
    {from: "baz"},
  ],
}

Other commands

All the available commands (you can also check with gmailctl help):

  apply       Apply a configuration file to Gmail settings
  debug       Shows an annotated version of the configuration
  diff        Shows a diff between the local configuration and Gmail settings
  download    Download filters from Gmail to a local config file
  edit        Edit the configuration and apply it to Gmail
  export      Export filters into the Gmail XML format
  help        Help about any command
  init        Initialize the Gmail configuration
  test        Execute config tests

Configuration

NOTE: Despite the name, the configuration format is stable at v1alpha3. If you are looking for the deprecated versions v1alpha1, or v1alpha2, please refer to docs/v1alpha1.md and docs/v1alpha2.md.

The configuration file is written in Jsonnet, that is a very powerful configuration language, derived from JSON. It adds functionality such as comments, variables, references, arithmetic and logic operations, functions, conditionals, importing other files, parameterizations and so on. For more details on the language, please refer to the official tutorial.

Simple example:

// Local variables help reuse config fragments
local me = {
  or: [
    { to: '[email protected]' },
    { to: '[email protected]' },
  ],
};

// The exported configuration starts here
{
  version: 'v1alpha3',
  // Optional author information (used in exports).
  author: {
    name: 'Pippo Pluto',
    email: '[email protected]'
  },
  rules: [
    {
      filter: {
        and: [
          { list: '[email protected]' },
          { not: me },  // Reference to the local variable 'me'
        ],
      },
      actions: {
        archive: true,
        labels: ['news'],
      },
    },
  ],
}

The Jsonnet configuration file contains mandatory version information, optional author metadata and a list of rules. Rules specify a filter expression and a set of actions that will be applied if the filter matches.

Filter operators are prefix of the operands they apply to. In the example above, the filter applies to emails that come from the mail list '[email protected]' AND the recipient is not 'me' (which can be '[email protected]' OR '[email protected]').

We will see all the features of the configuration file in the following sections.

Search operators

Search operators are the same as the ones you find in the Gmail filter interface:

  • from: the mail comes from the given address
  • to: the mail is delivered to the given address
  • subject: the subject contains the given words
  • has: the mail contains the given words

In addition to those visible in the Gmail interface, you can specify natively the following common operators:

  • list: the mail is directed to the given mail list
  • cc: the mail has the given address as CC destination
  • bcc: the mail has the given address as BCC destination
  • replyto: the mail has the given address as Reply-To destination

One more special function is given if you need to use less common operators1, or want to compose your query manually:

  • query: passes the given contents verbatim to the Gmail filter, without escaping or interpreting the contents in any way.

Example:

{
  version: 'v1alpha3',
  rules: [
    {
      filter: { subject: 'important mail' },
      actions: {
        markImportant: true,
      },
    },
    {
      filter: {
        query: 'dinner AROUND 5 friday has:spreadsheet',
      },
      actions: {
        delete: true,
      },
    },
  ],
}

Logic operators

Filters can contain only one expression. If you want to combine multiple of them in the same rule, you have to use logic operators (and, or, not). These operators do what you expect:

  • and: is true only if all the sub-expressions are also true
  • or: is true if one or more sub-expressions are true
  • not: is true if the sub-expression is false.

Example:

{
  version: 'v1alpha3',
  rules: [
    {
      filter: {
        or: [
          { from: 'foo' },
          {
            and: [
              { list: 'bar' },
              { not: { to: 'baz' } },
            ],
          },
        ],
      },
      actions: {
        markImportant: true,
      },
    },
  ],
}

This composite filter marks the incoming mail as important if:

  • the message comes from "foo", or
  • it is coming from the mailing list "bar" and not directed to "baz"

Reusing filters

Filters can be named and referenced in other filters. This allows reusing concepts and so avoid repetition. Note that this is not a gmailctl functionality but comes directly from the fact that we rely on Jsonnet.

Example:

local toMe = {
  or: [
    { to: '[email protected]' },
    { to: '[email protected]' },
  ],
};
local notToMe = { not: toMe };

{
  version: 'v1alpha3',
  rules: [
    {
      filter: {
        and: [
          { from: 'foobar' },
          notToMe,
        ],
      },
      actions: {
        delete: true,
      },
    },
    {
      filter: toMe,
      actions: {
        labels: ['directed'],
      },
    },
  ],
}

In this example, two named filters are defined. The toMe filter gives a name to emails directed to '[email protected]' or to '[email protected]'. The notToMe filter negates the toMe filter, with a not operator. Similarly, the two rules reference the two named filters above. The name reference is basically copying the definition of the filter in place.

The example is effectively equivalent to this one:

{
  version: 'v1alpha3',
  rules: [
    {
      filter: {
        and: [
          { from: 'foobar' },
          {
            not: {
              or: [
                { to: '[email protected]' },
                { to: '[email protected]' },
              ],
            },
          },
        ],
      },
      actions: {
        delete: true,
      },
    },
    {
      filter: {
        or: [
          { to: '[email protected]' },
          { to: '[email protected]' },
        ],
      },
      actions: {
        labels: ['directed'],
      },
    },
  ],
}

Relying on Jsonnet also allows importing code and raw data from other files3.

Actions

Every rule is a composition of a filter and a set of actions. Those actions will be applied to all the incoming emails that pass the rule's filter. These actions are the same as the ones in the Gmail interface:

  • archive: true: the message will skip the inbox;
  • delete: true: the message will go directly to the trash can;
  • markRead: true: the message will be mark as read automatically;
  • star: true: star the message;
  • markSpam: false: do never mark these messages as spam. Note that setting this field to true is not supported by Gmail (I don't know why);
  • markImportant: true: always mark the message as important, overriding Gmail heuristics;
  • markImportant: false: do never mark the message as important, overriding Gmail heuristics;
  • category: <CATEGORY>: force the message into a specific category (supported categories are "personal", "social", "updates", "forums", "promotions");
  • labels: [list, of, labels]: an array of labels to apply to the message. Note that these labels have to be already present in your settings (they won't be created automatically), and you can specify multiple labels (normally Gmail allows only one label per filter).
  • forward: '[email protected]': forward the message to another email address. The forwarding address must be already in your settings (Forwarding and POP/IMAP > Add a forwarding address). Gmail allows no more than 20 forwarding filters. Only one address can be specified for one filter.

Example:

{
  version: 'v1alpha3',
  rules: [
    {
      filter: { from: '[email protected]' },
      actions: {
        markImportant: true,
        category: 'personal',
        labels: ['family', 'P1'],
      },
    },
  ],
}

Labels

You can optionally manage your labels with gmailctl. The config contains a labels section. Adding labels in there will opt you in to full label management as well. If you prefer to manage your labels through the GMail web interface, you can by all means still do so by simply omitting the labels section from the config.

Example:

{
  version: 'v1alpha3',
  // optional
  labels: [
    { name: 'family' },
    { name: 'friends' },
  ],
  rules: [
    {
      filter: { from: '[email protected]' },
      actions: {
        labels: ['family'],
      },
    },
  ],
}

To make this work, your credentials need to contain permissions for labels management as well. If you configured gmailctl before this functionality was available, you probably need to update your 'Scopes for Google API' in the 'OAuth content screen' by adding https://www.googleapis.com/auth/gmail.labels. If you don't know how to do this, just reset and re-create your credentials following the steps in:

$ gmailctl init --reset
$ gmailctl init

If you want to update your existing config to include your existing labels, the best way to get started is to use the download command and copy paste the labels field into your config:

$ gmailctl download > /tmp/cfg.jsonnet
$ gmailctl edit

After the import, verify that your current config does not contain unwanted changes with gmailctl diff.

Managing the color of a label is optional. If you specify it, it will be enforced; if you don't, the existing color will be left intact. This is useful to people who want to keep setting the colors with the Gmail UI. You can find the list of supported colors here.

Example:

{
  version: 'v1alpha3',
  labels: [
    {
      name: 'family',
      color: {
        background: "#fad165",
        text: "#000000",
      },
    },
  ],
  rules: [ // ...
  ],
}

Note that renaming labels is not supported because there's no way to tell the difference between a rename and a deletion. This distinction is important because deleting a label and creating it with a new name would remove it from all the messages. This is a surprising behavior for some users, so it's currently gated by a confirmation prompt (for the edit command), or by the --remove-labels flag (for the apply command). If you want to rename a label, please do so through the GMail interface and then change your gmailctl config.

Tests

You can optionally add unit tests to your configuration. The tests will be executed before applying any changes to the upstream Gmail filters or by running the dedicated test subcommand. Tests results can be ignored by passing the --yolo command line option.

Tests can be added by using the tests field of the main configuration object:

{
  version: 'v1alpha3',
  rules: [ /* ... */ ],
  tests: [
    // you tests here.
  ],
}

A test object looks like this:

{
  // Reported when the test fails.
  name: "the name of the test",
  // A list of messages to test against.
  messages: [
    { /* message object */ },
    // ... more messages
  ],
  // The actions that should be applied to the messages, according the config.
  actions: {
    // Same as the Actions object in the filters.
  },
}

A message object is similar to a filter, but it doesn't allow arbitrary expressions, uses arrays of strings for certain fields (e.g. the to field), and has some additional fields (like body) to represent an email as faithfully as possible. This is the list of fields:

  • from: <string>: the sender of the email.
  • to: [<list>]: a list of recipients of the email.
  • cc: [<list>]: a list of emails in cc.
  • bcc: [<list>]: a list of emails in bcc.
  • replyto: <string>: the email listed in the Reply-To field.
  • lists: [<list>]: a list of mailing lists.
  • subject: <string>: the subject of the email.
  • body: <string>: the body of the email.

All the fields are optional. Remember that each message object represent one email and that the messages field of a test is an array of messages. A common mistake is to provide an array of messages thinking that they are only one. Example:

{
  // ...
  tests: [
    messages: [
      { from: "foobar" },
      { to: ["me"] },
    ],
    actions: {
      // ...
    },
  ],
}

This doesn't represent one message from "foobar" to "me", but two messages, one from "foobar" and the other to "me". The correct representation for that would be instead:

{
  // ...
  tests: [
    messages: [
      {
        from: "foobar",
        to: "me",
      },
    ],
    actions: {
      // ...
    },
  ],
}

NOTE: Not all filters are supported in tests. Arbitrary query expressions and filters with isEscaped: true are ignored by the tests. Warnings are generated when this happens. Keep in mind that in that case your tests might yield incorrect results.

Tips and tricks

Chain filtering

Gmail filters are all applied to a mail, if they match, in a non-specified order. So having some if-else alternative is pretty hard to encode by hand. For example sometimes you get interesting stuff from a mail list, but also a lot of garbage too. So, to put some emails with certain contents in one label and the rest somewhere else, you'd have to make multiple filters. Gmail filters however lack if-else constructs, so a way to simulate that is to declare a sequence of filters, where each one negates the previous alternatives.

For example you want to:

  • mark the email as important if directed to you;
  • or if it's coming from a list of favourite addresses, label as interesting;
  • or if it's directed to a certain alias, archive it.

Luckily you don't have to do that by hand, thanks to the utility library coming with gmailctl. There's a chainFilters function that does exactly that: takes a list of rules and chains them together, so if the first matches, the others are not applied, otherwise the second is checked, and so on...

// Import the standard library
local lib = import 'gmailctl.libsonnet';

local favourite = {
  or: [
    { from: '[email protected]' },
    { from: '[email protected]' },
    { list: '[email protected]' },
  ],
};

{
  version: 'v1alpha3',
  rules: [
           // ... Other filters applied in any order
         ]

         // And a chain of filters
         + lib.chainFilters([
           // All directed emails will be marked as important
           {
             filter: { to: '[email protected]' },
             actions: { markImportant: true },
           },
           // Otherwise, if they come from interesting senders, apply a label
           {
             filter: favourite,
             actions: { labels: ['interesting'] },
           },
           // Otherwise, if they are directed to my spam alias, archive
           {
             filter: { to: '[email protected]' },
             actions: { archive: true },
           },
         ]),
}

To me

Gmail gives you the possibility to write literally to:me in a filter, to match incoming emails where you are the recipient. This is going to mostly work as intended, except that it will also match emails directed to [email protected]. The risk you are getting an email where you are not one of the recipients, but a [email protected] is, is pretty low, but if you are paranoid you might consider using your full email instead. The config is also easier to read in my opinion. You can also save some typing by introducing a local variable like this:

// Local variable, referenced in all your config.
local me = '[email protected]';

{
  version: 'v1alpha3',
  rules: [
    {
      // Save typing here.
      filter: { to: me },
      actions: {
        markImportant: true,
      },
    },
  ],
}

Directly to me

If you need to match emails that are to you directly, (i.e. you are not in CC, or BCC, but only in the TO field), then the default Gmail filter to: [email protected] is not what you are looking for. This filter in fact (surprisingly) matches all the recipient fields (TO, CC, BCC). To make this work the intended way we have to pull out this trick:

local directlyTo(recipient) = {
  and: [
    { to: recipient },
    { not: { cc: recipient } },
    { not: { bcc: recipient } },
  ],
};

So, from all emails where your mail is a recipient, we remove the ones where your mail is in the CC field.

This trick is conveniently provided by the gmailctl library, so you can use it for example in this way:

// Import the standard library
local lib = import 'gmailctl.libsonnet';
local me = '[email protected]';
{
  version: 'v1alpha3',
  rules: [
    {
      filter: lib.directlyTo(me),
      actions: { markImportant: true },
    },
  ],
}

Automatic labels

If you opted in for labels management, you will find yourself often having to both add a filter and a label to your config. To alleviate this problem, you can use the utility function lib.rulesLabels provided with the gmailctl standard library. With that you can avoid providing the labels referenced by filters. They will be automatically added to the list of labels.

Example:

local lib = import 'gmailctl.libsonnet';
local rules = [
  {
    filter: { to: '[email protected]' },
    actions: { labels: ['directed'] },
  },
  {
    filter: { from: 'foobar' },
    actions: { labels: ['lists/foobar'] },
  },
  {
    filter: { list: 'baz' },
    actions: { labels: ['lists/baz', 'wow'] },
  },
];

// the config
{
  version: 'v1alpha3',
  rules: rules,
  labels: lib.rulesLabels(rules) + [{ name: l } for l in [
    'manual-label1',
    'priority',
    'priority/p1',
  ]],
}

The resulting list of labels will be:

labels: [
  // Automatically added
  { name: 'directed' },
  { name: 'lists' },            // Implied parent label
  { name: 'lists/baz' },
  { name: 'lists/foobar' },
  { name: 'wow' },
  // Manually added
  { name: 'manual-label1' },
  { name: 'priority' },
  { name: 'priority/p1' },
]

Note that there's no need to specify the label lists, because even if it's not used in any filter, it's the parent of a label that is used.

Things to keep in mind / gotchas:

  • Removing the last filter referencing a label will delete the label.
  • The only thing managed by the function is the list of labels names. You need to apply some transformations yourself if you want other properties (e.g. the color).
  • If you have labels that are not referenced by any filters (maybe archive labels, or labels applied manually). You have to remember to specify them manually in the list.

Thanks to legeana for the idea!

Multiple Gmail accounts

If you need to manage two or more accounts, it's useful to setup bash aliases this way:

alias gmailctlu1='gmailctl --config=$HOME/.gmailctlu1'
alias gmailctlu2='gmailctl --config=$HOME/.gmailctlu2'

You will then be able to configure both accounts separately by using one or the other alias.

Known issues

Apply filters to existing emails

gmailctl doesn't support this functionality for security reasons. The project currently needs only very basic permissisons, and applying filters to existing emails requires full Gmail access. Bugs in gmailctl or in your configuration won't screw up your old emails in any way, so this is an important safety feature. If you really want to do this, you can manually export your rules with gmailctl export > filters.xml, upload them by using the Gmail Settings UI and select the "apply new filters to existing email" checkbox.

OAuth2 authentication errors

Gmail APIs require strict controls, even if you are only accessing your own data. If you're getting errors similar to:

oauth2: cannot fetch token: 400 Bad Request
Response: {
  "error": "invalid_grant",
  "error_description": "Bad Request"
}

it's likely your auth token expired. Try refreshing it with:

$ gmailctl init --refresh-expired

and follow the instructions on screen.

If this doesn't help, retry the authorization workflow from the start:

$ gmailctl init --reset
$ gmailctl init

YAML config is unsupported

gmailctl recently deprecated older config versions (v1alpha1, v1alpha2). There's however a migration tool to port those into the latest Jsonnet format. To convert your config:

$ go run github.com/mbrt/gmailctl/cmd/gmailctl-config-migrate \
    ~/.gmailct/config.yaml > /tmp/gmailctl-config.jsonnet

Note: Adjust your paths if you're not keeping your config file in the default directory.

Confirm that the new config file doesn't have errors, nor shows diffs with your remote filters.

$ gmailctl diff -f /tmp/gmailctl-config.jsonnet

If everything looks good, replace the old with the new config:

$ mv /tmp/gmailctl-config.jsonnet ~/.gmailctl/config.jsonnet
$ rm ~/.gmailctl/config.yaml

Comparison with existing projects

gmail-britta has similar motivations and is quite popular. The difference between that project and this one are:

  • gmail-britta uses a custom DSL (versus Jsonnet in gmailctl)
  • gmail-britta is imperative because it allows you to write arbitrary Ruby code in your filters (versus pure declarative for gmailctl)
  • gmail-britta allows one to write complex chains of filters, but they feel very hardcoded and fails to provide easy ways to write reasonably easy filters 2.
  • gmail-britta exports only to the Gmail XML format. You have to import the filters yourself by using the Gmail web interface, manually delete the filters you updated and import only the new ones. This process becomes tedious very quickly and you will resort to quickly avoid using the tool when in a hurry. gmailctl provides you this possibility, but also allows you to review your changes and update the filters by using the Gmail APIs, without you having to do anything manually.
  • gmailctl tries to workaround certain limitations in Gmail (like applying multiple labels with the same filter) and provide a generic query language to Gmail, gmail-britta focuses on writing chain filtering and archiving in very few lines.

In short gmailctl takes the declarative approach to Gmail filters configuration, hoping it stays simpler to read and maintain, doesn't attempt to simplify complex scenarios with shortcuts (again, hoping the configuration becomes more readable) and provides automatic and fast updates to the filters that will save you time while you are iterating through new versions of your filters.

Footnotes

1: See Search operators you can use with Gmail .

2: Try to write the equivalent of this filter with gmail-britta

local spam = {
  or: [
    { from: '[email protected]' },
    { from: '[email protected]' },
    { subject: 'buy this' },
    { subject: 'buy that' },
  ],
};
{
  version: 'v1alpha3',
  rules: [
    {
      filter: spam,
      actions: { delete: true },
    },
  ],
}

It becomes something like this:

#!/usr/bin/env ruby

# NOTE: This file requires the latest master (30/07/2018) of gmail-britta.
# The Ruby repos are not up to date

require 'rubygems'
require 'gmail-britta'

SPAM_EMAILS = %w{[email protected] [email protected]}
SPAM_SUBJECTS = ['"buy this"', '"buy my awesome product"']

puts(GmailBritta.filterset(:me => MY_EMAILS) do
       # Spam
       filter {
         has [{:or => "from:(#{SPAM_EMAILS.join("|")})"}]
         delete_it
       }
       filter {
         has [{:or => "subject:(#{SPAM_SUBJECTS.join("|")})"}]
         delete_it
       }
     end.generate)

Not the most readable configuration I would say. Note: You also have to make sure to quote the terms correctly when they contain spaces.

So what about nesting expressions?

local me = '[email protected]';
local spam = {
  or: [
    { from: '[email protected]' },
    { from: '[email protected]' },
    { subject: 'buy this' },
    { subject: 'buy that' },
  ],
};
{
  version: 'v1alpha3',
  rules: [
    {
      filter: {
        and: [
          { to: me },
          { from: '[email protected]' },
          { not: spam },
        ],
      },
      actions: { delete: true },
    },
  ],
}

The reality is that you have to manually build the Gmail expressions yourself.

3: Import variables from a .libjsonnet file

File: spam.libjsonnet

{
  or: [
    { from: '[email protected]' },
    { from: '[email protected]' },
    { subject: 'buy this' },
    { subject: 'buy that' },
  ],
}

File config.jsonnet

local spam_filter = import 'spam.libjsonnet';
{
  version: 'v1alpha3',
  rules: [
    {
      filter: spam_filter,
      actions: { delete: true },
    },
  ],
}

gmailctl's People

Contributors

actions-user avatar bnjf avatar browniebroke avatar chenrui333 avatar chriscarini avatar dependabot-preview[bot] avatar dependabot[bot] avatar github-actions[bot] avatar gjolly avatar inwinw avatar ioayman avatar jonas-jonas avatar lutzky avatar matkoniecz avatar mbrt avatar mefuller avatar mig4 avatar mraakashshah avatar nick-merrill avatar paravoid avatar paxperscientiam avatar pbechu avatar peteroneilljr avatar raybb avatar rcassani avatar robhung avatar slix avatar studgeek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gmailctl's Issues

Add List filter

Filtering mailing lists can be done by using the has operator with list:(mymaillist-address).

We can currently workaround that by using:

  - filters:
      has:
        - list:(fd84c1c757e02889a9b08d289.122677.list-id.mcsv.net)

Ideally you would like to use instead:

  - filters:
      list:
        - fd84c1c757e02889a9b08d289.122677.list-id.mcsv.net

is this mail filter management supporting creating filters that will be in a specific order

I started testing Gmail API (with simplest possible Python script based on their tutorial) and discovered that sequentially added filters are appearing on filter list in a random order.

Is this tool using API in way that also results in the same situation?

I have some filters that relied on import to Gmail keeping order and now I am looking for a solution. I starting to worry that there is no one but maybe gmailctl has some miraculous way of ensuring stable order.

Add "forward" action (to forward emails)

"Forward it to: Choose an address.." from Gmail's filter creation is missing as an Action option in gmailctl.

The Gmail API supports it as action.forward. The parameter is a string containing the forwarding email. I confirmed this with the API explorer.

However, forward is different because it's not a pseudo-label like gmailctl's other actions.

user.settings.filters.create returns an error if the forwarding address is not set up on the user's account:

{
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "failedPrecondition",
    "message": "Unrecognized forwarding address"
   }
  ],
  "code": 400,
  "message": "Unrecognized forwarding address"
 }
}

The same error happens for malformed input, like @ or [][]. The empty string succeeds as if forward was absent.

Should we care about the case where a user wants one filter to forward to multiple email addresses? It's probably rare, and I'm not sure how Gmail would react to the duplicate queries with different forwarding addresses.

I'm blocked from using gmailctl for my personal email because I have 3 filters that forward my email. I forward my award point statements to AwardWallet (so that it doesn't have permissions to read my entire mailbox).

I looked at the code a bit, and I may try implementing this in the next week or so.

Use jsonnet for config

Instead of adding more and more features to the config file, it's better to rely on a proper configuration language: https://jsonnet.org/

This will require a new version of the config format.

Consider adding installation prerequisites

I suspect that my go version is outdated, but maybe something else went wrong (I am trying to run go program for the first time in my life).

mateusz@grisznak:~/Desktop/tmp$ go install github.com/mbrt/gmailctl/cmd/gmailctl
can't load package: package github.com/mbrt/gmailctl/cmd/gmailctl: cannot find package "github.com/mbrt/gmailctl/cmd/gmailctl" in any of:
	/usr/lib/go-1.6/src/github.com/mbrt/gmailctl/cmd/gmailctl (from $GOROOT)
	($GOPATH not set)
mateusz@grisznak:~/Desktop/tmp$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH=""
GORACE=""
GOROOT="/usr/lib/go-1.6"
GOTOOLDIR="/usr/lib/go-1.6/pkg/tool/linux_amd64"
GO15VENDOREXPERIMENT="1"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"

Add expression filter

To enable complex filters we should add an expression operator, that enables custom queries (see
expressions docs).

We could additionally allow variables that refer to global consts to make it even fancier.

Error applying configuration: syntax error in config file

I've imported my current configuration and then tried to edit the rules, but while saving the following message pops up:

Error applying configuration: syntax error in config file: error parsing the config version: yaml: line 3: mapping values are not allowed in this context
Do you want to continue editing? [y/N]: 

Don't know why he thinks it is a yaml.

Config file:

// Auto-imported filters by 'gmailctl download'.
//
// WARNING: This functionality is experimental. Before making any
// changes, check that no diff is detected with the remote filters by
// using the 'diff' command.

// Uncomment if you want to use the standard library.
// local lib = import 'gmailctl.libsonnet';
{
  version: "v1alpha3",
  author: {
    name: "YOUR NAME HERE (auto imported)",
    email: "[email protected]"
  },
  // Note: labels management is optional. If you prefer to use the
  // GMail interface to add and remove labels, you can safely remove
  // this section of the config.
  labels: [
    {
      name: "Belege"
    },
    {
      name: "Reisen"
    },
    {
      name: "[Imap]/Trash"
    },
    {
      name: "Privat"
    },
    {
      name: "Geschäftlich"
    }
  ],
  rules: null
}

diff not showing differences

I downloaded my filters from gmail and the diff command shows no diffs.
However when I look in gmail I have filters like the following:

Matches: list:foo.bar.com -{(to:[email protected])}
Do this: Skip Inbox

Matches: list:foo.bar.com
Do this: Apply label "list"

The downloaded filters look like:

    {
      filter: {
        query: "list:foo.bar.com"
      },
      actions: {
        archive: true
      }
    },
    {
      filter: {
        query: "list:foo.bar.com"
      },
      actions: {
        labels: [
          "list"
        ]
      }
    },

and seem to have lost the part excluding emails sent directly to me.

Am I misunderstanding how the config is supposed to work?

Improve error reporting

I've got a problem in my rules:

cannot parse config file: error parsing criteria for rule #20: empty filter node

...which is probably accurate, but having to carefully count forward 20 rules by hand, especially in the face of functions, is much more challenging than I'd like.

It'd be great if this dumped out the JSON or equivalent of the rule. That won't give the original jsonnet for it, of course, but I'd expect it has a very high chance of including enough identifying details that would make it fast to track down the error.

This is an "application level" error, incidentally: the JSON generates correctly, but I'm presumably missing something in there.

write filter that runs in order

Thank you for wonderful project! I have more then 70 filters in gmail and using such tool is really helpful.
My current problem that i have some filters for single "to" address that can fills to multiple labels , does it possible to write something that if filter matches - it labeled and stop processing other filters?
This is really can helps in situations when you need to have big "not" stuff inside many filters. (i'm don't like to see one email in multiple labels)

document required scopes for gmailctl

gmailctl requires only one specific scope to function:

https://www.googleapis.com/auth/gmail.settings.basic

It'd be nice to document that, rather than advising to create what amounts to read-write access to everything during project setup. (Also, best practice, of course.)

If you do what I asked in #54 then you would also need

https://www.googleapis.com/auth/gmail.labels

You can find the specific scopes by hitting the reference pages, such as:
https://developers.google.com/gmail/api/v1/reference/users/labels/create#auth

jsonnet helper for filter creation

I've been thinking about adding some helper functions to the gmailctl.libsonnet library.

In particular, I'm looking on a way to create valid structured filters using the logic operators. May be something that could be used like this,

local gmailctl = import 'gmailctl.libsonnet';
local Filter = gmailctl.Filter;

Filter.new()
=>
{}

Filter.new().and({to: "[email protected]"}).values()
=>
{
  and: [{to: "[email protected]"}]
}

The idea would be to be able to use method chaining to build and reuse filters. You could do something like

local to_me = Filter
				.new()
				.and({to: "[email protected]"});

local chained_filter = Filter
						.new()
						.and({subject: "special email"})
						.or(to_me);

chained_filter.values();
=>
{
	and: [{subject: "special email"],
	or: [{ and: [{to: "[email protected]"}]]
}

I'm a noob in jsonnet, I've been trying to implement something like this with some success. I'd love to know your thoughts on the idea, implementation, and d actual syntaxis.

diff crashes when a rule has no actions

To reproduce:

  1. In gmail, create a label "deleteme"
  2. In gmail, create a rule that applies label "deleteme" and performs no other actions
  3. Delete label "deleteme"
  4. Run gmailctl diff

This causes the following stack trace:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x8e22a8]

goroutine 1 [running]:
github.com/mbrt/gmailctl/pkg/export/api.defaultImporter.importAction(0x0, 0xb82260, 0xc0005ac450, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/pkg/export/api/api_import.go:65 +0x58
github.com/mbrt/gmailctl/pkg/export/api.defaultImporter.importFilter(0xc0007aea20, 0xb82260, 0xc0005ac450, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/pkg/export/api/api_import.go:48 +0x8e
github.com/mbrt/gmailctl/pkg/export/api.defaultImporter.Import(0xc00058a200, 0x3e, 0x3f, 0xb82260, 0xc0005ac450, 0x0, 0x7fd80111a000, 0xc0001da5a0, 0xc0001f6700, 0xc0001ec0d8)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/pkg/export/api/api_import.go:34 +0xeb
github.com/mbrt/gmailctl/pkg/api.(*gmailAPI).ListFilters(0xc000220760, 0xc000220760, 0x0, 0x0, 0xc00065cec8, 0x8)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/pkg/api/api.go:42 +0x1bc
github.com/mbrt/gmailctl/cmd/gmailctl/cmd.diff(0xc000024440, 0x36, 0xc000024440, 0x36)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/cmd/gmailctl/cmd/diff_cmd.go:54 +0x18e
github.com/mbrt/gmailctl/cmd/gmailctl/cmd.glob..func3(0x11ee1a0, 0x1219318, 0x0, 0x0)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/cmd/gmailctl/cmd/diff_cmd.go:30 +0x42
github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra.(*Command).execute(0x11ee1a0, 0x1219318, 0x0, 0x0, 0x11ee1a0, 0x1219318)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra/command.go:766 +0x2ae
github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra.(*Command).ExecuteC(0x11eeb20, 0x0, 0xac397b, 0x2d)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra/command.go:852 +0x2c0
github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra.(*Command).Execute(...)
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/vendor/github.com/spf13/cobra/command.go:800
github.com/mbrt/gmailctl/cmd/gmailctl/cmd.Execute()
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/cmd/gmailctl/cmd/root_cmd.go:30 +0x32
main.main()
        /usr/local/google/home/lutzky/go/src/github.com/mbrt/gmailctl/cmd/gmailctl/main.go:6 +0x20

Instead, gmailctl should probably just ignore that rule in the importer. The gmail UI attempts to block you from creating rules that have no actions.

askYN methods default 'N' does not behave as expected

When (for example) you run gmailctl apply you are prompted with

Do you want to apply them? [y/N]:

The Linux style of [y/N] suggests that the default behaviour when just hitting the Return key will be 'N' and nothing will happen. But currently, this flow will get you stuck.

To reproduce, make a change, run gmailctl apply and when prompted just hit return.

Tested on Mac with iTerm

I suspect this originates in the askYN method that uses fmt.Scanln that gives an error if you just hit return.

I will submit a PR with an alternative for fmt.Scanln using bufio ReadString

Deprecate YAML format for config

The YAML configuration file is not much used anymore as it's way less useful than the Jsonnet one. We should get rid of it and as we are at it deprecate named filters as well.

Include an option to continue editing instead of applying filters

I just spent about an hour writing a fairly complicated set of rules, then saved the file and checked the diff and it didn't look quite right. So I typed n and re-ran gmailctl edit and was quite shocked to see all my work had disappeared...

It would be nice if there was another option other than y/n: perhaps e to re-open the file. Also an indication that n will discard your changes.

Add command that exports test URLs for filters

The URL with search results of a certain query in Gmail doesn't change between users. We could exploit that by showing URLs that the user can use to test if their filters are correct.

Diff can be a bit unclear

I have this as a filter in config.jsonnet:

      filter: {
        and: [
          {
            from: '@paypal.com',
          },
          {
            or: [
              {
                subject: 'Receipt for *',
                isEscaped: true,
              },
              {
                subject: 'authorized a payment',
                isEscaped: true,
              },
            ],
          },
        ],
      },

When I run gmailctl diff, that or branch is shown as:

+    subject: {Receipt for * authorized a payment}

It makes it look like the subject Receipt for * authorized a payment is being matched, rather than Receipt for * or authorized a payment.

Clearer or updated instructions

1. Create a new project if you don't have one
1. Go to 'Enable API and services' and select Gmail
2. Go to credentials and create a new one, by selecting 'Help me choose'
   2a. Select the Gmail API
   2b. Select 'Other UI'
   2c. Access 'User data'.
3. Go to 'OAuth constent screen' and update 'Scopes for Google API', by
   adding:
     * https://www.googleapis.com/auth/gmail.labels
     * https://www.googleapis.com/auth/gmail.metadata
     * https://www.googleapis.com/auth/gmail.settings.basic
4. IMPORTANT: you don't need to submit your changes for verification, as
   you're not creating a public App
5. Download the credentials file into '/Users/tcurdt/.gmailctl/credentials.json'
   and execute the 'init' command again.

This is not covered:

Screen Shot 2020-03-29 at 15 43 15

After step 4 it says

Screen Shot 2020-03-29 at 15 40 40

and there is no way to "Save" without adding more information.

Duplicated filters

Generated filters are sometimes duplicated. Still needs to investigate why.

Add support for "Never send it to spam"

The "never send it to spam" action is not supported at the moment. This makes also import fail whenever Gmail decides to modify your filters and add this action. [Not sure why this happens].

Missing newline at the end of export

Using gmailctl export prints the xml without newline at the end. This causes the last line to be missing from the terminal. It works when piped to some other program.

Allow downloading filters from GMail.

When starting to use gmailctl you get an initial, example template list. It would be really useful if you could download your existing filters so you could tweak them instead of starting afresh.

Add bcc filter operator

So that a user can write bcc: 'someone@something' instead of query: 'bcc: "someone@something"'.

Gmail labels are not refreshed when 'continue editing' is chosen

Repro:

  1. gmailctl edit
  2. Add a new rule using a label that does not exist
    -> edit fails, user presented with option 'Do you want to continue editing?'
  3. Choose 'y'
  4. Add the label in gmail (leaving the editor open)
  5. Try to go ahead with the edit
    -> Still results in the same error (Label does not exist)

Expected: edit should go through once the label is added

Note: if you choose to abort the editing, the changes you made to the config are lost, or at least there's no obvious way to recover them

Add user config tests

Users should be able to define their tests. Example:

{
  rules: [ /* rules */ ],
  tests: [
    {
      messages: [
        {
          subject: "Some message",
          to: "[email protected]",
          /* more fields */,
        },
        /* other messages */
      ],
      actions: {
        // expected effect of all actions from all matching rules
        labels: ['spammy-alerts'],
        archive: true,
      },
    },
    // more tests
  ],
}

To avoid re-implementing too much of gmail filters logic, we could restrict the checks to well known operators and "ban" the raw queries altogether.

We should also add a new test command in the gmailctl cli.

list available actions

Available filters are listed, unfortunately, I see nowhere clear list of what actions can be applied and their codes

Is it possible to control the order of filters?

gmailctl is a wonderful tool! I've been searching for something like this for a long time. Thank you for creating it.

I created a config file with about 20 or so filters. When I apply the config fild to my Gmail account, the filters do not remain in the order I have them in the config file. The order in the Gmail UI seems to be random.

I don't need the filters to be in order. My concern is not at all related to the sequence in which the filters are applied. I would like the filters in the Gmail UI to reflect the order of the filters in my config file, because that would help me manage them better. For example, if something is going wrong with a filter, I would be able to more quickly and easily find it in the Gmail UI, if the filters were in my preferred order.

Thanks, again, for creating this great tool.

[P.S. I apologize for not adding a tag to this post. I intended to label it as a question. I see on the right side it says, 'Labels: None yet', but I don't see how to give the post a label.]

Docs for v1alpha2

The current documentation explains v1alpha1. We should update it with the new version.

gmailctl.libsonnet update

When the user runs gmailctl init the gmailctl.libsonnet is created. Unfortunately the file it never updated automatically again.
I propose gmailctl init --reset-lib or similar flag to force update all library files (current and future).
It is understandable that gmailctl will not rewrite config.jsonnet as it may contain user configuration, however library files are not expected to be touched by the user.

intersection by default

This is a feature request.

Current:

{
  filter: {
    and: [
      { from: 'ticketsroleaccount <[email protected]>' },
      {
        not: { to: me }
      },
    ],
  },
}

I would like:

{
  filter: {
    from: 'ticketsroleaccount <[email protected]>',
    not: { to: me },
  },
}

This behavior is same as search, and is backward compatible (test whether there are more than one key).

Is there a way to set a variable with both "to" and "from" email addresses?

As I said in a previous post, thank you for this great tool.

I have a set of co-workers, and I have a filter that applies a label to messages received from these co-workers or sent to these co-workers. The list of co-workers includes about twenty people.

To setup this filter in the config file, I created two variables. The first variable looks like this:

local fromCoworkers = {
or: [
{ from: '[email protected]'},
{ from: '[email protected]'},
{ from: '[email protected]'},
...
{ from: '[email protected]'},
],
};

The second variable looks like this:

local toCoworkers = {
or: [
{ to: '[email protected]'},
{ to: '[email protected]'},
{ to: '[email protected]'},
...
{ to: '[email protected]'},
],
};

And the filter rule looks like this:

{
  filter: {
    or: [
      fromCoworkers,
      toCoworkers,          
    ],
  },
  actions: {
    labels: [
      "Coworkers"
    ]
  }
},

This works, but I have to maintain two versions of the same list. I have to list all twenty email addresses in the fromCoworkers variable and then the same set of email addresses in the toCoworkers variable.

Is there a way to do this, so that I only have to maintain one list?

Thanks.

[As with my previous post, I intended to label this as a question. I just read the Github help files about applying labels to issues. It looks like only the owner of a project can apply labels. Anyway, I hope I'm not doing this wrong by not applying a label.]

Add lint command

A simple lint command would be nice for just checking to see if the file is properly formed. Currently I use debug, but it would be nice to check the file without all that output in the terminal.

Subject (and possibly other rules) with quotes

Quotes inside subject produce unexpected result, they basically remove quoting. Escaping is not really supported in GMail so it would be nice to print an error message if this happens to not confuse users.

    {   
      filter: {
        subject: '"hello world"',
      },  
      actions: {
        labels: ["testing"],
      },  
    },  

--- Current
+++ TO BE APPLIED
@@ -1 +1,5 @@
+* Criteria:
+    subject: ""hello world""
+  Actions:
+    apply label: testing
 

Forward slash in labels is unsupported

Gmail supports labels with forward slashes in the name, without mandating the parent label to be there. For example a label could be called "foo/bar" and "foo" doesn't have to be present for this to work.

The current validation is a bit restrictive and demands "foo" to be in the list of labels as well. This was to avoid users' confusion, but removes the use case. Since Gmail doesn't complain about this case, gmailctl shouldn't complain too.

Add simplify pass for typical "archiving" filters

Filters of the form:

{A B C D} -{E F G H}

Could be used as a catch all archive rule (e.g. either of these mail lists but not directed to me, or with this subject line).

This filter can become pretty big and so ignored by Gmail. Rewriting it into a set of filters like this:

A -{E F G H}
B -{E F G H}
...

Would solve the issue (if the exclusion list is short enough).

We should automatically apply this transformation in the SimplifyCriteria step.

Tests perform overly exact matching on some fields

For example, if I have a test where one of the messages has to: ['some-list'], and I have a rule filtering for to: ['some-list'], the test thinks that this rule is applied to that message. However, if the message has to: ['[email protected]'], the test things that this rule is not applied to that message; in real-gmail-filtering it is.

Test failures should show actions diffs

Sample error message:

config tests failed: test 'stuff' failed: message #0 is going to get unexpected actions: {"archive":true,"labels":["stuff"]}

The test was configured to just check for labels: ["stuff"]. A test failure would look nicer like so:

config tests failed: test 'stuff' failed: message #0 is going to get unexpected actions, -want +got:
 {
+  "archive": true,
   "labels":["stuff"]
 }

This should be easy using https://github.com/google/go-cmp.

Auto-create missing labels on apply

I just applied my shiny new rule and ...

error adding filters: error exporting filter #0: error in export action: label 'redacted' not found

...ouch. It'd be nice if labels could be auto-created when required.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.