gmail-britta 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,6 +13,7 @@ require 'logger'
13
13
  require 'gmail-britta/single_write_accessors'
14
14
  require 'gmail-britta/filter_set'
15
15
  require 'gmail-britta/filter'
16
+ require 'gmail-britta/chaining_filter'
16
17
 
17
18
  # # A generator DSL for importable gmail filter specifications.
18
19
  #
@@ -23,6 +24,7 @@ module GmailBritta
23
24
  # This is the main entry point for GmailBritta.
24
25
  # @option opts :me [Array<String>] A list of email addresses that should be considered as belonging to "you", effectively those email addresses you would expect `to:me` to match.
25
26
  # @option opts :logger [Logger] (Logger.new()) An initialized logger instance.
27
+ # @options opts :author [Hash] The author of the gmail filters. The hash has :name and :email keys
26
28
  # @yield the filterset definition block. `self` inside the block is the {FilterSet} instance.
27
29
  # @return [FilterSet] the constructed filterset
28
30
  def self.filterset(opts={}, &block)
@@ -0,0 +1,82 @@
1
+ module GmailBritta
2
+ class ChainingFilter < Filter
3
+ attr_reader :parent
4
+ def merged?; @merged ; end
5
+
6
+ def initialize(parent)
7
+ @parent = parent
8
+ super(parent.filterset, :log => parent.logger)
9
+ end
10
+
11
+ def generate_xml
12
+ ensure_merged_with_parent
13
+ super
14
+ end
15
+
16
+ # TODO: Maybe just extend #perform to merge after it's done.
17
+ def log_definition
18
+ return unless @log.debug?
19
+
20
+ ensure_merged_with_parent
21
+ super
22
+ end
23
+
24
+ def ensure_merged_with_parent
25
+ unless merged?
26
+ @merged = true
27
+ perform_merge(@parent)
28
+ end
29
+ end
30
+ end
31
+
32
+ class NegatedChainingFilter < ChainingFilter
33
+ def initialize(parent)
34
+ super
35
+ end
36
+
37
+ def perform_merge(filter)
38
+ def load(name, filter)
39
+ filter.send("get_#{name}").reject do |elt|
40
+ instance_variable_get("@#{name}").member?(elt)
41
+ end
42
+ end
43
+
44
+ def invert(old)
45
+ old.map! do |addr|
46
+ if addr[0] == '-'
47
+ addr[1..-1]
48
+ else
49
+ '-' + addr
50
+ end
51
+ end
52
+ end
53
+
54
+ def deep_invert(has_not, has)
55
+ case
56
+ when has_not.first.is_a?(Hash) && has_not.first[:or]
57
+ has_not.first[:or] += has
58
+ has_not
59
+ when has_not.length > 0
60
+ [{:or => has_not + has}]
61
+ else
62
+ has
63
+ end
64
+ end
65
+
66
+ @to += invert(load(:to, filter))
67
+ @from += invert(load(:from, filter))
68
+ @has_not += deep_invert(load(:has_not, filter), load(:has, filter))
69
+ end
70
+ end
71
+
72
+ class PositiveChainingFilter < ChainingFilter
73
+ def initialize(parent)
74
+ super
75
+ end
76
+
77
+ def perform_merge(filter)
78
+ @has += filter.get_has
79
+ @has_not += filter.get_has_not
80
+ end
81
+ end
82
+ end
@@ -46,6 +46,25 @@ module GmailBritta
46
46
  # @param [String] label the label to assign the message
47
47
  single_write_accessor :label, 'label'
48
48
 
49
+ # Assign the given smart label to the message
50
+ # @return [void]
51
+ # @!method smart_label(category)
52
+ # @param [String] category the smart label to assign the message
53
+ single_write_accessor :smart_label, 'smartLabelToApply' do |category|
54
+ case category
55
+ when 'forums', 'Forums'
56
+ '^smartlabel_group'
57
+ when 'notifications', 'Notifications', 'updates', 'Updates'
58
+ '^smartlabel_notification'
59
+ when 'promotions', 'Promotions'
60
+ '^smartlabel_promo'
61
+ when 'social', 'Social'
62
+ '^smartlabel_social'
63
+ else
64
+ raise 'invalid category "' << category << '"'
65
+ end
66
+ end
67
+
49
68
  # Forward the message to the given label.
50
69
  # @return [void]
51
70
  # @!method forward_to(email)
@@ -65,6 +84,37 @@ module GmailBritta
65
84
  emit_filter_spec(list)
66
85
  end
67
86
 
87
+ # @!method from(conditions)
88
+ # @return [void]
89
+ # Defines the positive conditions for the filter to match.
90
+ # Uses: <apps:property name='from' value='postman@usps.gov'></apps:property>
91
+ # Instead of: <apps:property name='hasTheWord' value='from:postman@usps.gov'></apps:property>
92
+ single_write_accessor :from, 'from' do |list|
93
+ emit_filter_spec(list)
94
+ end
95
+
96
+ # @!method to(conditions)
97
+ # @return [void]
98
+ # Defines the positive conditions for the filter to match.
99
+ # Uses: <apps:property name='to' value='postman@usps.gov'></apps:property>
100
+ # Instead of: <apps:property name='hasTheWord' value='to:postman@usps.gov'></apps:property>
101
+ single_write_accessor :to, 'to' do |list|
102
+ emit_filter_spec(list)
103
+ end
104
+
105
+ # @!method subject(conditions)
106
+ # @return [void]
107
+ # Defines the positive conditions for the filter to match.
108
+ # @overload subject([conditions])
109
+ # Conditions ANDed together that an incoming email must match.
110
+ # @param [Array<conditions>] conditions a list of gmail search terms, all of which must match
111
+ # @overload subject({:or => [conditions]})
112
+ # Conditions ORed together for the filter to match
113
+ # @param [{:or => conditions}] conditions a hash of the form `{:or => [condition1, condition2]}` - either of these conditions must match to match the filter.
114
+ single_write_accessor :subject, 'subject' do |list|
115
+ emit_filter_spec(list)
116
+ end
117
+
68
118
  # @!method has_not(conditions)
69
119
  # @return [void]
70
120
  # Defines the negative conditions that must not match for the filter to be allowed to match.
@@ -77,16 +127,20 @@ module GmailBritta
77
127
  single_write_boolean_accessor :has_attachment, 'hasAttachment'
78
128
  # @!endgroup
79
129
 
130
+ #@!group Filter chaining
131
+ def chain(type, &block)
132
+ filter = type.new(self).perform(&block)
133
+ filter.log_definition
134
+ filter
135
+ end
136
+
80
137
  # Register and return a new filter that matches only if this
81
- # filter's conditions (those that are not duplicated on the new
82
- # filter's `has` clause) do not match.
138
+ # Filter's conditions (those that are not duplicated on the new
139
+ # Filter's {#has} clause) *do not* match.
83
140
  # @yield The filter definition block
84
141
  # @return [Filter] the new filter
85
142
  def otherwise(&block)
86
- filter = Filter.new(@britta, :log => @log).perform(&block)
87
- filter.merge_negated_criteria(self)
88
- filter.log_definition
89
- filter
143
+ chain(NegatedChainingFilter, &block)
90
144
  end
91
145
 
92
146
  # Register and return a new filter that matches a message only if
@@ -95,58 +149,64 @@ module GmailBritta
95
149
  # @yield The filter definition block
96
150
  # @return [Filter] the new filter
97
151
  def also(&block)
98
- filter = Filter.new(@britta, :log => @log).perform(&block)
99
- filter.merge_positive_criteria(self)
100
- filter.log_definition
101
- filter
152
+ chain(PositiveChainingFilter, &block)
102
153
  end
103
154
 
104
155
  # Register (but don't return) a filter that archives the message
105
156
  # unless it matches the `:to` email addresses. Optionally, mark
106
157
  # the message as read if this filter matches.
107
158
  #
108
- # @note This method returns the previous filter to make it easier to construct filter chains with {#otherwise} and {#also}.
159
+ # @note This method returns the previous filter to make it easier
160
+ # to construct filter chains with {#otherwise} and {#also}
161
+ # with {#archive_unless_directed} in the middle.
109
162
  #
110
163
  # @option options [true, false] :mark_read If true, mark the message as read
111
164
  # @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}.
112
- # @return [Filter] the current (not the newly-constructed filter)
165
+ # @return [Filter] `self` (not the newly-constructed filter)
113
166
  def archive_unless_directed(options={})
114
167
  mark_as_read=options[:mark_read]
115
168
  tos=Array(options[:to] || me)
116
- filter = Filter.new(@britta, :log => @log).perform do
169
+ filter = PositiveChainingFilter.new(self).perform do
117
170
  has_not [{:or => tos.map {|to| "to:#{to}"}}]
118
171
  archive
119
172
  if mark_as_read
120
173
  mark_read
121
174
  end
122
175
  end
123
- filter.merge_positive_criteria(self)
124
176
  filter.log_definition
125
177
  self
126
178
  end
179
+ #@!endgroup
127
180
 
128
181
  # Create a new filter object
129
- # @note Over the lifetime of {GmailBritta}, new {Filter}s usually get created only by the {Delegate}.
182
+ # @note Over the lifetime of {GmailBritta}, new {Filter}s usually get created only by the {FilterSet::Delegate}.
130
183
  # @param [GmailBritta::Britta] britta the filterset object
131
184
  # @option options :log [Logger] a logger for debug messages
132
185
  def initialize(britta, options={})
133
186
  @britta = britta
134
187
  @log = options[:log]
188
+ @from = []
189
+ @to = []
190
+ @has = []
191
+ @has_not = []
135
192
  end
136
193
 
137
194
  # Return the filter's value as XML text.
138
195
  # @return [String] the Atom XML representation of this filter
139
196
  def generate_xml
140
- engine = Haml::Engine.new(<<-ATOM)
197
+ generate_xml_properties
198
+ engine = Haml::Engine.new("
141
199
  %entry
142
200
  %category{:term => 'filter'}
143
201
  %title Mail Filter
144
202
  %content
145
- - self.class.single_write_accessors.keys.each do |name|
146
- - gmail_name = self.class.single_write_accessors[name]
147
- - if value = self.send("output_\#{name}".intern)
148
- %apps:property{:name => gmail_name, :value => value.to_s}
149
- ATOM
203
+ #{generate_haml_properties 1}
204
+ ", :attr_wrapper => '"')
205
+ engine.render(self)
206
+ end
207
+
208
+ def generate_xml_properties
209
+ engine = Haml::Engine.new(generate_haml_properties, :attr_wrapper => '"')
150
210
  engine.render(self)
151
211
  end
152
212
 
@@ -162,59 +222,39 @@ ATOM
162
222
  end
163
223
 
164
224
  protected
225
+ def filterset; @britta; end
165
226
 
166
- def merge_negated_criteria(filter)
167
- old_has_not = Marshal.load(Marshal.dump((filter.get_has_not || []).reject { |elt|
168
- @has.member?(elt)
169
- }))
170
- old_has = Marshal.load( Marshal.dump((filter.get_has || []).reject { |elt|
171
- @has.member?(elt)
172
- }))
173
- @log.debug(" M: oh #{old_has.inspect}")
174
- @log.debug(" M: ohn #{old_has_not.inspect}")
175
-
176
- @has_not ||= []
177
- @has_not += case
178
- when old_has_not.first.is_a?(Hash) && old_has_not.first[:or]
179
- old_has_not.first[:or] += old_has
180
- old_has_not
181
- when old_has_not.length > 0
182
- [{:or => old_has_not + old_has}]
183
- else
184
- old_has
185
- end
186
- @log.debug(" M: h #{@has.inspect}")
187
- @log.debug(" M: nhn #{@has_not.inspect}")
188
- end
189
-
190
- def merge_positive_criteria(filter)
191
- new_has = (@has || []) + (filter.get_has || [])
192
- new_has_not = (@has_not || []) + (filter.get_has_not || [])
193
- @has = new_has
194
- @has_not = new_has_not
195
- end
227
+ def logger; @log ; end
196
228
 
197
229
  def self.emit_filter_spec(filter, infix=' ', recursive=false)
198
- str = ''
199
230
  case filter
200
231
  when String
201
- str << filter
232
+ filter
202
233
  when Hash
234
+ str = ''
203
235
  filter.keys.each do |key|
236
+ infix = ' '
237
+ prefix = ''
204
238
  case key
205
239
  when :or
206
- str << emit_filter_spec(filter[key], ' OR ', recursive)
240
+ infix = ' OR '
241
+ when :and
242
+ infix = ' AND '
207
243
  when :not
208
- str << '-'
209
- str << emit_filter_spec(filter[key], ' ', true)
244
+ prefix = '-'
245
+ recursive = true
210
246
  end
247
+ str << prefix + emit_filter_spec(filter[key], infix, recursive)
211
248
  end
249
+ str
212
250
  when Array
213
- str << '(' if recursive
214
- str << filter.map {|elt| emit_filter_spec(elt, ' ', true)}.join(infix)
215
- str << ')' if recursive
251
+ str_tmp = filter.map {|elt| emit_filter_spec(elt, ' ', true)}.join(infix)
252
+ if recursive
253
+ "(#{str_tmp})"
254
+ else
255
+ str_tmp
256
+ end
216
257
  end
217
- str
218
258
  end
219
259
 
220
260
  # Note a filter definition on the logger.
@@ -222,8 +262,8 @@ ATOM
222
262
  def log_definition
223
263
  return unless @log.debug?
224
264
  @log.debug "Filter: #{self}"
225
- Filter.single_write_accessors.each do |name, gmail_name|
226
- val = instance_variable_get(Filter.ivar_name(name))
265
+ Filter.single_write_accessors.keys.each do |name, gmail_name|
266
+ val = send(:"get_#{name}")
227
267
  @log.debug " #{name}: #{val}" if val
228
268
  end
229
269
  self
@@ -233,5 +273,20 @@ ATOM
233
273
  def me
234
274
  @britta.me
235
275
  end
276
+
277
+ private
278
+
279
+ def generate_haml_properties(indent=0)
280
+ properties =
281
+ "- self.class.single_write_accessors.keys.each do |name|
282
+ - gmail_name = self.class.single_write_accessors[name]
283
+ - if value = self.send(\"output_\#{name}\".intern)
284
+ %apps:property{:name => gmail_name, :value => value.to_s}"
285
+ if (indent)
286
+ indent_sp = ' '*indent*2
287
+ properties = indent_sp + properties.split("\n").join("\n" + indent_sp)
288
+ end
289
+ properties
290
+ end
236
291
  end
237
292
  end
@@ -4,6 +4,9 @@ module GmailBritta
4
4
  @filters = []
5
5
  @me = opts[:me] || 'me'
6
6
  @logger = opts[:logger] || allocate_logger
7
+ @author = opts[:author] || {}
8
+ @author[:name] ||= "Andreas Fuchs"
9
+ @author[:email] ||= "asf@boinkor.net"
7
10
  end
8
11
 
9
12
  # Currently defined filters
@@ -38,8 +41,8 @@ module GmailBritta
38
41
  %id tag:mail.google.com,2008:filters:
39
42
  %updated #{Time.now.utc.iso8601}
40
43
  %author
41
- %name Andreas Fuchs
42
- %email asf@boinkor.net
44
+ %name #{@author[:name]}
45
+ %email #{@author[:email]}
43
46
  - filters.each do |filter|
44
47
  != filter.generate_xml
45
48
  ATOM
@@ -1,54 +1,87 @@
1
1
  module GmailBritta
2
+ # This mixin defines a simple convenience methods for creating
3
+ # accessors that can only be written to once for each instance.
2
4
  module SingleWriteAccessors
5
+ # @!parse extend SingleWriteAccessors::ClassMethods
3
6
  module ClassMethods
4
- def ivar_name(name)
5
- "@#{name}".intern
6
- end
7
7
 
8
+ # @return [Array<Symbol>] the single write accessors defined on
9
+ # this class and every superclass.
8
10
  def single_write_accessors
9
- @single_write_accessors ||= {}
11
+ super_accessors = {}
12
+ if self.superclass.respond_to?(:single_write_accessors)
13
+ super_accessors = self.superclass.single_write_accessors
14
+ end
15
+ super_accessors.merge(direct_single_write_accessors)
10
16
  end
11
17
 
18
+ # Defines a string-typed filter accessor DSL method. Generates
19
+ # the `[name]`, `get_[name]` and `output_[name]` methods.
20
+ # @param name [Symbol] the name of the accessor method
21
+ # @param gmail_name [String] the name of the attribute in the
22
+ # gmail Atom export
12
23
  def single_write_accessor(name, gmail_name, &block)
13
- single_write_accessors[name] = gmail_name
14
- ivar_name = self.ivar_name(name)
24
+ direct_single_write_accessors[name] = gmail_name
25
+ ivar = ivar_name(name)
15
26
  define_method(name) do |words|
16
- if instance_variable_get(ivar_name)
27
+ if instance_variable_get(ivar) and instance_variable_get(ivar) != []
17
28
  raise "Only one use of #{name} is permitted per filter"
18
29
  end
19
- instance_variable_set(ivar_name, words)
20
- end
21
- define_method("get_#{name}") do
22
- instance_variable_get(ivar_name)
30
+ instance_variable_set(ivar, words)
23
31
  end
32
+ get(name, ivar)
24
33
  if block_given?
25
34
  define_method("output_#{name}") do
26
- instance_variable_get(ivar_name) && block.call(instance_variable_get(ivar_name))
35
+ instance_variable_get(ivar) && block.call(instance_variable_get(ivar)) unless instance_variable_get(ivar) == []
27
36
  end
28
37
  else
29
- define_method("output_#{name}") do
30
- instance_variable_get(ivar_name)
31
- end
38
+ output(name, ivar)
32
39
  end
33
40
  end
34
41
 
42
+ # Defines a boolean-typed filter accessor DSL method. If the
43
+ # method gets called in the filter definition block, that causes
44
+ # the value to switch to `true`.
45
+ # @note There is no way to turn these boolean values back off in
46
+ # Gmail's export XML.
47
+ # @param name [Symbol] the name of the accessor method
48
+ # @param gmail_name [String] the name of the attribute in the
49
+ # gmail Atom export
35
50
  def single_write_boolean_accessor(name, gmail_name)
36
- single_write_accessors[name] = gmail_name
37
- ivar_name = self.ivar_name(name)
51
+ direct_single_write_accessors[name] = gmail_name
52
+ ivar = ivar_name(name)
38
53
  define_method(name) do |*args|
39
54
  value = args.length > 0 ? args[0] : true
40
- if instance_variable_get(ivar_name)
55
+ if instance_variable_get(ivar)
41
56
  raise "Only one use of #{name} is permitted per filter"
42
57
  end
43
- instance_variable_set(ivar_name, value)
58
+ instance_variable_set(ivar, value)
44
59
  end
60
+ get(name, ivar)
61
+ output(name, ivar)
62
+ end
63
+
64
+
65
+ private
66
+ def ivar_name(name)
67
+ :"@#{name}"
68
+ end
69
+
70
+ def get(name, ivar)
45
71
  define_method("get_#{name}") do
46
- instance_variable_get(ivar_name)
72
+ instance_variable_get(ivar)
47
73
  end
74
+ end
75
+
76
+ def output(name, ivar)
48
77
  define_method("output_#{name}") do
49
- instance_variable_get(ivar_name)
78
+ instance_variable_get(ivar)
50
79
  end
51
80
  end
81
+
82
+ def direct_single_write_accessors
83
+ @direct_single_write_accessors ||= {}
84
+ end
52
85
  end
53
86
 
54
87
  # @!visibility private