gmail-britta 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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