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.
- checksums.yaml +7 -0
- data/.travis.yml +5 -8
- data/.yardopts +2 -1
- data/Gemfile +3 -2
- data/Gemfile.lock +66 -21
- data/README.md +3 -1
- data/THANKS.md +15 -0
- data/TODO.org +6 -0
- data/VERSION +1 -1
- data/examples/Gemfile.lock +2 -2
- data/examples/asf.rb +29 -2
- data/gmail-britta.gemspec +14 -11
- data/lib/gmail-britta.rb +2 -0
- data/lib/gmail-britta/chaining_filter.rb +82 -0
- data/lib/gmail-britta/filter.rb +117 -62
- data/lib/gmail-britta/filter_set.rb +5 -2
- data/lib/gmail-britta/single_write_accessors.rb +54 -21
- data/test/test_gmail-britta.rb +249 -21
- metadata +24 -39
data/lib/gmail-britta.rb
CHANGED
@@ -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
|
data/lib/gmail-britta/filter.rb
CHANGED
@@ -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
|
-
#
|
82
|
-
#
|
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
|
-
|
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
|
-
|
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
|
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]
|
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 =
|
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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
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
|
-
|
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
|
-
|
240
|
+
infix = ' OR '
|
241
|
+
when :and
|
242
|
+
infix = ' AND '
|
207
243
|
when :not
|
208
|
-
|
209
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
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 =
|
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
|
42
|
-
%email
|
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
|
-
|
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
|
-
|
14
|
-
|
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(
|
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(
|
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(
|
35
|
+
instance_variable_get(ivar) && block.call(instance_variable_get(ivar)) unless instance_variable_get(ivar) == []
|
27
36
|
end
|
28
37
|
else
|
29
|
-
|
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
|
-
|
37
|
-
|
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(
|
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(
|
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(
|
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(
|
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
|