Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

A DSL for slightly smarter gmail filters.

This successfully generates a pretty complicated filter setup that
can separate humans from machine-generated emails, and looks good
doing it.

No docs or examples yet (I feel like I shouldn't publish my workplace
email filters), coming soon.
  • Loading branch information...
commit ba999c0bb3c8ffba989b1369cdb414c8fb1d99b4 0 parents
Andreas Fuchs authored

Showing 2 changed files with 260 additions and 0 deletions. Show diff stats Hide diff stats

  1. +1 0  .gitignore
  2. +259 0 lib/gmail-britta.rb
1  .gitignore
... ... @@ -0,0 +1 @@
  1 +run-*.rb
259 lib/gmail-britta.rb
... ... @@ -0,0 +1,259 @@
  1 +#!/usr/bin/env ruby
  2 +
  3 +# Google mail exclusive filter generator
  4 +# Docs: http://groups.google.com/group/gmail-labs-help-filter-import-export/browse_thread/thread/518a7b1634f20cdb#
  5 +# http://code.google.com/googleapps/domain/email_settings/developers_guide_protocol.html#GA_email_filter_main
  6 +
  7 +require 'rubygems'
  8 +require 'time'
  9 +gem 'haml'
  10 +require 'haml'
  11 +require 'logger'
  12 +
  13 +$log = Logger.new(STDERR)
  14 +$log.level = Logger::DEBUG
  15 +
  16 +module SingleWriteAccessors
  17 + module ClassMethods
  18 + def ivar_name(name)
  19 + "@#{name}".intern
  20 + end
  21 +
  22 + def single_write_accessors
  23 + @single_write_accessors ||= {}
  24 + end
  25 +
  26 + def single_write_accessor(name, gmail_name, &block)
  27 + single_write_accessors[name] = gmail_name
  28 + ivar_name = self.ivar_name(name)
  29 + define_method(name) do |words|
  30 + if instance_variable_get(ivar_name)
  31 + raise "Only one use of #{name} is permitted per filter"
  32 + end
  33 + instance_variable_set(ivar_name, words)
  34 + end
  35 + define_method("get_#{name}") do
  36 + instance_variable_get(ivar_name)
  37 + end
  38 + if block_given?
  39 + define_method("output_#{name}") do
  40 + instance_variable_get(ivar_name) && block.call(instance_variable_get(ivar_name))
  41 + end
  42 + else
  43 + define_method("output_#{name}") do
  44 + instance_variable_get(ivar_name)
  45 + end
  46 + end
  47 + end
  48 +
  49 + def single_write_boolean_accessor(name, gmail_name)
  50 + single_write_accessors[name] = gmail_name
  51 + ivar_name = self.ivar_name(name)
  52 + define_method(name) do |*args|
  53 + value = args.length > 0 ? args[0] : true
  54 + if instance_variable_get(ivar_name)
  55 + raise "Only one use of #{name} is permitted per filter"
  56 + end
  57 + instance_variable_set(ivar_name, value)
  58 + end
  59 + define_method("get_#{name}") do
  60 + instance_variable_get(ivar_name)
  61 + end
  62 + define_method("output_#{name}") do
  63 + instance_variable_get(ivar_name)
  64 + end
  65 + end
  66 + end
  67 +
  68 + def self.included(base)
  69 + base.extend(ClassMethods)
  70 + end
  71 +end
  72 +
  73 +class GmailBritta
  74 + def initialize
  75 + @filters = []
  76 + end
  77 +
  78 + attr_accessor :filters
  79 +
  80 + def self.filterset(&block)
  81 + (britta = GmailBritta.new).rules(&block)
  82 + britta
  83 + end
  84 +
  85 + def rules(&block)
  86 + Delegate.new(self).perform(&block)
  87 + end
  88 +
  89 + class Delegate
  90 + def initialize(britta)
  91 + @britta = britta
  92 + @filter = nil
  93 + end
  94 +
  95 + def filter(&block)
  96 + Filter.new(@britta).perform(&block)
  97 + end
  98 +
  99 + def perform(&block)
  100 + instance_eval(&block)
  101 + end
  102 + end
  103 +
  104 + class Filter
  105 + include SingleWriteAccessors
  106 + single_write_accessor :has, 'hasTheWord' do |list|
  107 + emit_filter_spec(list)
  108 + end
  109 + single_write_accessor :has_not, 'doesNotHaveTheWord' do |list|
  110 + emit_filter_spec(list)
  111 + end
  112 + single_write_boolean_accessor :archive, 'shouldArchive'
  113 + single_write_boolean_accessor :mark_read, 'shouldMarkAsRead'
  114 + single_write_boolean_accessor :mark_important, 'shouldAlwaysMarkAsImportant'
  115 + single_write_boolean_accessor :mark_unimportant, 'shouldNeverMarkAsImportant'
  116 + single_write_boolean_accessor :star, 'shouldStar'
  117 + single_write_boolean_accessor :never_spam, 'shouldNeverSpam'
  118 + single_write_accessor :label, 'label'
  119 + single_write_accessor :forward_to, 'forwardTo'
  120 +
  121 + def generate_xml
  122 + engine = Haml::Engine.new(<<-ATOM)
  123 +%entry
  124 + %category{:term => 'filter'}
  125 + %title Mail Filter
  126 + %content
  127 + - self.class.single_write_accessors.keys.each do |name|
  128 + - gmail_name = self.class.single_write_accessors[name]
  129 + - if value = self.send("output_\#{name}".intern)
  130 + %apps:property{:name => gmail_name, :value => value.to_s}
  131 +ATOM
  132 + engine.render(self)
  133 + end
  134 +
  135 + def self.emit_filter_spec(filter, infix=' ')
  136 + str = ''
  137 + case filter
  138 + when String
  139 + str << filter
  140 + when Hash
  141 + filter.keys.each do |key|
  142 + case key
  143 + when :or
  144 + str << '('
  145 + str << emit_filter_spec(filter[key], ' OR ')
  146 + str << ')'
  147 + when :not
  148 + str << '-('
  149 + str << emit_filter_spec(filter[key], ' ')
  150 + str << ')'
  151 + end
  152 + end
  153 + when Array
  154 + str << filter.map {|elt| emit_filter_spec(elt, ' ')}.join(infix)
  155 + end
  156 + $log.debug " Filter spec #{filter.inspect} + #{infix.inspect} => #{str.inspect}"
  157 + str
  158 + end
  159 +
  160 + def initialize(britta)
  161 + @britta=britta
  162 + end
  163 +
  164 + def log_definition
  165 + $log.debug "Filter: #{self}"
  166 + Filter.single_write_accessors.each do |name|
  167 + val = instance_variable_get(Filter.ivar_name(name))
  168 + $log.debug " #{name}: #{val}" if val
  169 + end
  170 + self
  171 + end
  172 +
  173 + def perform(&block)
  174 + instance_eval(&block)
  175 + @britta.filters << self
  176 + self
  177 + end
  178 +
  179 + def merge_negated_criteria(filter)
  180 + old_has_not = Marshal.load(Marshal.dump((filter.get_has_not || []).reject { |elt|
  181 + @has.member?(elt)
  182 + }))
  183 + old_has = Marshal.load( Marshal.dump((filter.get_has || []).reject { |elt|
  184 + @has.member?(elt)
  185 + }))
  186 + $log.debug(" M: oh #{old_has.inspect}")
  187 + $log.debug(" M: ohn #{old_has_not.inspect}")
  188 +
  189 + @has_not ||= []
  190 + @has_not += case
  191 + when old_has_not.first.is_a?(Hash) && old_has_not.first[:or]
  192 + old_has_not.first[:or] += old_has
  193 + old_has_not
  194 + when old_has_not.length > 0
  195 + [{:or => old_has_not + old_has}]
  196 + else
  197 + old_has
  198 + end
  199 + $log.debug(" M: h #{@has.inspect}")
  200 + $log.debug(" M: nhn #{@has_not.inspect}")
  201 + end
  202 +
  203 + def otherwise(&block)
  204 + filter = Filter.new(@britta).perform(&block)
  205 + filter.merge_negated_criteria(self)
  206 + filter.log_definition
  207 + filter
  208 + end
  209 +
  210 + def merge_positive_criteria(filter)
  211 + new_has = (@has || []) + (filter.get_has || [])
  212 + new_has_not = (@has_not || []) + (filter.get_has_not || [])
  213 + @has = new_has
  214 + @has_not = new_has_not
  215 + end
  216 +
  217 + def also(&block)
  218 + filter = Filter.new(@britta).perform(&block)
  219 + filter.merge_positive_criteria(self)
  220 + filter.log_definition
  221 + filter
  222 + end
  223 +
  224 + def archive_unless_directed(options={})
  225 + mark_as_read=options[:mark_read]
  226 + to=options[:to] || 'me'
  227 + filter = Filter.new(@britta).perform do
  228 + has_not %W(to:#{to})
  229 + archive
  230 + if mark_as_read
  231 + mark_read
  232 + end
  233 + end
  234 + filter.merge_positive_criteria(self)
  235 + filter.log_definition
  236 + self
  237 + end
  238 + end
  239 +
  240 + def generate
  241 + engine = Haml::Engine.new(<<-ATOM)
  242 +!!! XML
  243 +%feed{:xmlns => 'http://www.w3.org/2005/Atom', 'xmlns:apps' => 'http://schemas.google.com/apps/2006'}
  244 + %title Mail Filters
  245 + %id tag:mail.google.com,2008:filters:
  246 + %updated #{Time.now.utc.iso8601}
  247 + %author
  248 + %name Andreas Fuchs
  249 + %email asf@boinkor.net
  250 + - filters.each do |filter|
  251 + != filter.generate_xml
  252 +ATOM
  253 + engine.render(self)
  254 + end
  255 +end
  256 +
  257 +
  258 +
  259 +

0 comments on commit ba999c0

Please sign in to comment.
Something went wrong with that request. Please try again.