lib/gmail-britta/filter.rb
module GmailBritta
# This class specifies the behavior of a single filter
# definition. Create a filter object in the DSL via
# {FilterSet::Delegator#filter} or use {Filter#also},
# {Filter#otherwise} or {Filter#archive_unless_directed} to make a
# new filter based on another one.
# @todo this probably needs some explanatory docs to make it understandable.
class Filter
include SingleWriteAccessors
# @!group Criteria for matching messages in filter blocks
# @!method has(conditions)
# @return [void]
# Defines the positive conditions for the filter to match.
# @overload has([conditions])
# Conditions ANDed together that an incoming email must match.
# @param [Array<conditions>] conditions a list of gmail search terms, all of which must match
# @overload has({:or => [conditions]})
# Conditions ORed together for the filter to match
# @param [{:or => conditions}] conditions a hash of the form `{:or => [condition1, condition2]}` - either of these conditions must match to match the filter.
define_criteria :has, 'hasTheWord' do |list|
emit_filter_spec(list)
end
# @!method from(conditions)
# @return [void]
# Defines the positive conditions for the filter to match.
# Uses: <apps:property name='from' value='postman@usps.gov'></apps:property>
# Instead of: <apps:property name='hasTheWord' value='from:postman@usps.gov'></apps:property>
define_criteria :from, 'from' do |list|
emit_filter_spec(list)
end
# @!method to(conditions)
# @return [void]
# Defines the positive conditions for the filter to match.
# Uses: <apps:property name='to' value='postman@usps.gov'></apps:property>
# Instead of: <apps:property name='hasTheWord' value='to:postman@usps.gov'></apps:property>
define_criteria :to, 'to' do |list|
emit_filter_spec(list)
end
# @!method subject(conditions)
# @return [void]
# Defines the positive conditions for the filter to match.
# @overload subject([conditions])
# Conditions ANDed together that an incoming email must match.
# @param [Array<conditions>] conditions a list of gmail search terms, all of which must match
# @overload subject({:or => [conditions]})
# Conditions ORed together for the filter to match
# @param [{:or => conditions}] conditions a hash of the form `{:or => [condition1, condition2]}` - either of these conditions must match to match the filter.
define_criteria :subject, 'subject' do |list|
emit_filter_spec(list)
end
# @!method has_not(conditions)
# @return [void]
# Defines the negative conditions that must not match for the filter to be allowed to match.
define_criteria :has_not, 'doesNotHaveTheWord' do |list|
emit_filter_spec(list)
end
# Filter for messages that have an attachment
# @macro bool_dsl_method
define_boolean_criteria :has_attachment, 'hasAttachment'
# @!endgroup
# @!group Actions to take on messages in filter blocks
# Archive the message.
# @!macro [new] bool_dsl_method
# @return [void]
# @!method $1()
single_write_boolean_accessor :archive, 'shouldArchive'
# Move the message to the trash.
# @macro bool_dsl_method
single_write_boolean_accessor :delete_it, 'shouldTrash'
# Mark the message as read.
# @macro bool_dsl_method
single_write_boolean_accessor :mark_read, 'shouldMarkAsRead'
# Mark the message as important.
# @macro bool_dsl_method
single_write_boolean_accessor :mark_important, 'shouldAlwaysMarkAsImportant'
# Do not mark the message as important.
# @macro bool_dsl_method
single_write_boolean_accessor :mark_unimportant, 'shouldNeverMarkAsImportant'
# Star the message
# @macro bool_dsl_method
single_write_boolean_accessor :star, 'shouldStar'
# Never mark the message as spam
# @macro bool_dsl_method
single_write_boolean_accessor :never_spam, 'shouldNeverSpam'
# Assign the given label to the message
# @return [void]
# @!method label(label)
# @param [String] label the label to assign the message
single_write_accessor :label, 'label'
# Assign the given smart label to the message
# @return [void]
# @!method smart_label(category)
# @param [String] category the smart label to assign the message
single_write_accessor :smart_label, 'smartLabelToApply' do |category|
case category
when 'personal', 'Personal'
'^smartlabel_personal'
when 'forums', 'Forums'
'^smartlabel_group'
when 'notifications', 'Notifications', 'updates', 'Updates'
'^smartlabel_notification'
when 'promotions', 'Promotions'
'^smartlabel_promo'
when 'social', 'Social'
'^smartlabel_social'
else
raise 'invalid category "' << category << '"'
end
end
# Forward the message to the given label.
# @return [void]
# @!method forward_to(email)
# @param [String] email an email address to forward the message to
single_write_accessor :forward_to, 'forwardTo'
# @!endgroup
#@!group Filter chaining
def chain(type, &block)
filter = type.new(self).perform(&block)
filter.log_definition
filter
end
def criteria
self.class.single_write_criteria.keys.reduce({}) do |res, name|
ivar = "@#{name.to_s}"
if instance_variable_defined?(ivar)
res[name] = instance_variable_get(ivar)
end
res
end
end
# Register and return a new filter that matches only if this
# Filter's conditions (those that are not duplicated on the new
# Filter's {#has} clause) *do not* match.
# @yield The filter definition block
# @return [Filter] the new filter
def otherwise(&block)
chain(NegatedChainingFilter, &block)
end
# Register and return a new filter that matches a message only if
# this filter's conditions *and* the previous filter's condition
# match.
# @yield The filter definition block
# @return [Filter] the new filter
def also(&block)
chain(PositiveChainingFilter, &block)
end
# Register (but don't return) a filter that archives the message
# unless it matches the `:to` email addresses. Optionally, mark
# the message as read if this filter matches.
#
# @note This method returns the previous filter to make it easier
# to construct filter chains with {#otherwise} and {#also}
# with {#archive_unless_directed} in the middle.
#
# @option options [true, false] :mark_read If true, mark the message as read
# @option options [Array<String>] :to a list of addresses that the message may be addressed to in order to prevent this filter from matching. Defaults to the value given to :me on {GmailBritta.filterset}.
# @return [Filter] `self` (not the newly-constructed filter)
def archive_unless_directed(options={})
mark_as_read=options[:mark_read]
tos=Array(options[:to] || me)
filter = PositiveChainingFilter.new(self).perform do
has_not [{:or => tos.map {|to| "to:#{to}"}}]
archive
if mark_as_read
mark_read
end
end
filter.log_definition
self
end
#@!endgroup
# Create a new filter object
# @note Over the lifetime of {GmailBritta}, new {Filter}s usually get created only by the {FilterSet::Delegate}.
# @param [GmailBritta::Britta] britta the filterset object
# @option options :log [Logger] a logger for debug messages
def initialize(britta, options={})
@britta = britta
@log = options[:log]
@from = []
@to = []
@has = []
@has_not = []
@subject = []
end
# Return the filter's value as XML text.
# @return [String] the Atom XML representation of this filter
def generate_xml
generate_xml_properties
engine = Haml::Engine.new("
%entry
%category{:term => 'filter'}
%title Mail Filter
%content
#{generate_haml_properties 1}
", :attr_wrapper => '"')
engine.render(self)
end
def generate_xml_properties
engine = Haml::Engine.new(generate_haml_properties, :attr_wrapper => '"')
engine.render(self)
end
# Evaluate block as a filter definition block and register `self` as a filter on the containing {FilterSet}
# @note this method gets called by {Delegate#filter} to create and register a new filter object
# @yield The filter definition. `self` in the block is the new filter object.
# @api private
# @return [Filter] the filter that
def perform(&block)
instance_eval(&block)
@britta.filters << self
self
end
protected
def filterset; @britta; end
def logger; @log ; end
def self.emit_filter_spec(filter, infix=' ', recursive=false)
case filter
when String
if recursive && filter =~ /\s/
# filters can be parts of OR groups, which means whitespace
# is significant. Let's properly group these:
"(#{filter})"
else
filter
end
when Hash
str = ''
filter.keys.each do |key|
infix = ' '
prefix = ''
case key
when :or
infix = ' OR '
when :and
infix = ' AND '
when :not
prefix = '-'
recursive = true
end
str << prefix + emit_filter_spec(filter[key], infix, recursive)
end
str
when Array
str_tmp = filter.map {|elt| emit_filter_spec(elt, ' ', true)}.join(infix)
if recursive
"(#{str_tmp})"
else
str_tmp
end
end
end
# Note a filter definition on the logger.
# @note for debugging only.
def log_definition
return unless @log.debug?
@log.debug "Filter: #{self}"
Filter.single_write_accessors.keys.each do |name, gmail_name|
val = send(:"get_#{name}")
@log.debug " #{name}: #{val}" if val
end
self
end
# Return the list of emails that the filterset has configured as "me".
def me
@britta.me
end
private
def generate_haml_properties(indent=0)
properties =
"- self.class.single_write_accessors.keys.each do |name|
- gmail_name = self.class.single_write_accessors[name]
- if self.send(\"defined_\#{name}?\".intern)
- value = self.send(\"output_\#{name}\".intern)
%apps:property{:name => gmail_name, :value => value.to_s}"
if (indent)
indent_sp = ' '*indent*2
properties = indent_sp + properties.split("\n").join("\n" + indent_sp)
end
properties
end
end
end