gmail-britta 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile CHANGED
@@ -8,6 +8,8 @@ gem 'haml', '~> 3.1.6'
8
8
  group :development do
9
9
  gem "shoulda", ">= 0"
10
10
  gem "rdoc", "~> 3.12"
11
+ gem "yard", '~> 0.8.3'
12
+ gem 'redcarpet', '~> 2.2.2'
11
13
  gem "bundler", "~> 1.2.0"
12
14
  gem "jeweler", "~> 1.8.4"
13
15
  gem "rcov", ">= 0"
data/Gemfile.lock CHANGED
@@ -20,12 +20,14 @@ GEM
20
20
  rcov (1.0.0)
21
21
  rdoc (3.12)
22
22
  json (~> 1.4)
23
+ redcarpet (2.2.2)
23
24
  shoulda (3.1.1)
24
25
  shoulda-context (~> 1.0)
25
26
  shoulda-matchers (~> 1.2)
26
27
  shoulda-context (1.0.0)
27
28
  shoulda-matchers (1.3.0)
28
29
  activesupport (>= 3.0.0)
30
+ yard (0.8.3)
29
31
 
30
32
  PLATFORMS
31
33
  ruby
@@ -38,4 +40,6 @@ DEPENDENCIES
38
40
  nokogiri
39
41
  rcov
40
42
  rdoc (~> 3.12)
43
+ redcarpet (~> 2.2.2)
41
44
  shoulda
45
+ yard (~> 0.8.3)
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # gmail-britta
2
+
3
+ This library helps you generate XML that you can import into gmail's
4
+ filter settings. It makes it more pleasant to write simple (and even
5
+ complex) filter chains.
6
+
7
+ ## Motivation
8
+
9
+ As a user of the internet, you probably use gmail, and you probably
10
+ get a lot of Email - I do. Just like me, you're also likely to have a
11
+ lot of filters that were once supposed to keep your inbox
12
+ clean. But every time you click "Filter messages like this", you
13
+ remember the enormous list of filters you have and how it is only ever
14
+ going to make things worse in the long run.
15
+
16
+ ### But how do you make them better?
17
+
18
+ As a programmer, I write code. As a metaprogrammer, I write code that
19
+ writes code. If only gmail filters could be written by a program. Oh
20
+ wait, [they
21
+ can](http://gmailblog.blogspot.com/2009/03/new-in-labs-filter-importexport.html).
22
+ There's an import & export function for gmail filters. These filters
23
+ are exported as XML, and the import in turn takes XML files. What this
24
+ library does is generate XML that you can import.
25
+
26
+ ## Examples
27
+
28
+ Here are some very simple examples of how gmail-britta can make your
29
+ life easier:
30
+
31
+ ### Archiving (or not) semi-important email
32
+
33
+ Imagine you're on a discussion mailing list that you skim from time to
34
+ time - so you want to label email to that list, and archive email from
35
+ it so it doesn't clog up your inbox. Now, from time to time someone
36
+ will Cc: you an important message, and you don't want to miss these -
37
+ what to do?
38
+
39
+ Naively, you define a filter like this:
40
+
41
+ ```
42
+ list:discuss@lists.some-side-project.org !to:thisisme@mydomain.com
43
+ -> Archive, Label "some-side-project"
44
+ ```
45
+
46
+ But this also means email addressed to you but also sent to that list
47
+ will be missing the "some-side-project" label. This means you need two
48
+ filters!
49
+
50
+ ```
51
+ list:discuss@lists.some-side-project.org !to:thisisme@mydomain.com
52
+ -> Archive
53
+ list:discuss@lists.some-side-project.org
54
+ -> Label "some-side-project"
55
+ ```
56
+
57
+ Now this can get pretty hairy pretty quickly; and if you ever have to
58
+ change the filter (e.g., the list changes email addresses), you have
59
+ to change two filters! Ugh.
60
+
61
+ With gmail-britta, it's just this:
62
+
63
+ ``` ruby
64
+ GmailBritta.filterset do
65
+ filter {
66
+ has %w{list:discuss@lists.some-side-project.org}
67
+ }.archive_unless_directed
68
+ end
69
+ ```
70
+
71
+ That's it - this little piece of ruby code will generate these two
72
+ filters; and if the list address changes, you just re-import these
73
+ filters. Easy.
74
+
75
+ ### Better detection of your own email address
76
+
77
+ If you have issued a search for `to:me` on gmail in the last few
78
+ months (and you get Email from iCloud nee MobileMe users), you'll
79
+ notice that `to:me` also matches the "me" in `example@me.com`. This is
80
+ very frustrating mostly because any filter as above that matches on
81
+ `to:me` will now also not archive email sent from a me.com
82
+ account. With gmail-britta, you can define a set of emails that are
83
+ yours, and `archive_unless_directed` will generate a filter that
84
+ matches on those:
85
+
86
+ ``` ruby
87
+ GmailBritta.filterset(:me => ['thisisme@my-private.org', 'this-is-me@bigco.example.com']) do
88
+ filter {
89
+ has %w{list:discuss@lists.some-side-project.org}
90
+ }.archive_unless_directed
91
+ end
92
+ ```
93
+
94
+ ### If/else chains on filters
95
+
96
+ There are instances where you can't only match on the mailing list -
97
+ sometimes interesting stuff will be sent to one list (that you want to
98
+ read), and lots of unnecessary silliness will be sent there too. So,
99
+ to put email with a certain subject into one label, and to put all the
100
+ rest into another, you have to make multiple filters. But gmail lacks
101
+ if/else in filters - you have to duplicate and correctly negate each
102
+ filter's condition in the next one. Luckily, you have gmail-britta to
103
+ help you out there:
104
+
105
+ ``` ruby
106
+ GmailBritta.filterset do
107
+ filter {
108
+ has %w{list:robots@bigco.com subject:Important}
109
+ label 'work/robots/important'
110
+ # Do not archive
111
+ }.otherwise {
112
+ has %w{list:robots@bigco.com subject:Chunder}
113
+ label 'work/robots/irrelevant'
114
+ }.archive_unless_directed(:mark_read => true).otherwise {
115
+ has %w{list:robots@bigco.com subject:Semirelevant}
116
+ label 'work/robots/meh'
117
+ }.archive_unless_directed
118
+ end
119
+ ```
120
+
121
+ A lot of things going on here! First, I hope you notice that this
122
+ looks similar to an if/else-if statement. Second, you may notice the
123
+ liberal application of `.archive_unless_directed`, with an optional
124
+ `:mark_read` parameter. Third, do check out the XML generated by this,
125
+ it's pretty horriffic - but the filters do exactly what the code says:
126
+
127
+ 1. If the email is sent to that list and has "Important" in the subject, it gets labeled with the work/robots/important label, and won't be archived (unless another filter further down does archive that email).
128
+ 2. If the email does not match all the above criteria, but is sent to that list and has "Chunder" in the subject, label it irrelevant, archive it and mark the email as read.
129
+ 3. If the email does not match all the above criteria, but is sent to that list and has "Semirelevant" in the subject, label it "meh" and archive it, but don't mark it as read.
130
+
131
+ ## Installation & usage
132
+
133
+ Best to use gmail-britta from a ruby script; but install the gem first:
134
+
135
+ gem install gmail-britta
136
+
137
+ Or include it in your Gemfile:
138
+
139
+ gem 'gmail-britta'
140
+
141
+ This is a simple script that generates filters for you:
142
+
143
+ ``` ruby
144
+ #!/bin/env ruby
145
+
146
+ require 'rubygems'
147
+ require 'gmail-britta'
148
+
149
+ fs = GmailBritta.filterset(:me => ['thisisme@my-private.org']) do
150
+ filter {
151
+ has %w{from:asf@boinkor.net}
152
+ label 'from-the-author-of-gmail-britta'
153
+ never_spam
154
+ }
155
+ end
156
+ puts fs.generate
157
+ ```
158
+
159
+ Running this script will write to stdout the filter XML that you
160
+ need. Redirect that to a file and upload it on the bottom of your
161
+ gmail filter settings, and you'll label email from me and archive it
162
+ unless I email you specifically. Not that useful, but with your own
163
+ filters and the recipes above, you should be able to make it work (-:
164
+
165
+ ## A short apology to you, dear code-diver
166
+
167
+ A lot of this is a bit hacky (particularly the filter condition merge
168
+ logic), as it started as a oneoff ruby script - very sorry for any
169
+ amateurish code you may encounter. There are few tests, but so far, it
170
+ does work for me. Let me know if this made your life harder or easier!
171
+ (-:
172
+
173
+ ## Contributing to gmail-britta
174
+
175
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
176
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
177
+ * Fork the project.
178
+ * Start a feature/bugfix branch.
179
+ * Commit and push until you are happy with your contribution.
180
+ * Make sure to add tests for it. (Note that are only very few tests yet. I would totally appreciate if you started fleshing out this suite, but I wouldn't expect you to add much of anything.)
181
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
182
+
183
+ ## Copyright
184
+
185
+ Copyright (c) 2012 Andreas Fuchs. See LICENSE.txt for
186
+ further details.
data/Rakefile CHANGED
@@ -42,12 +42,6 @@ end
42
42
 
43
43
  task :default => :test
44
44
 
45
- require 'rdoc/task'
46
- Rake::RDocTask.new do |rdoc|
47
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+ require 'yard'
48
46
 
49
- rdoc.rdoc_dir = 'rdoc'
50
- rdoc.title = "gmail-britta #{version}"
51
- rdoc.rdoc_files.include('README*')
52
- rdoc.rdoc_files.include('lib/**/*.rb')
53
- end
47
+ YARD::Rake::YardocTask.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
data/gmail-britta.gemspec CHANGED
@@ -5,29 +5,30 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "gmail-britta"
8
- s.version = "0.1.4"
8
+ s.version = "0.1.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andreas Fuchs"]
12
- s.date = "2012-12-22"
12
+ s.date = "2012-12-23"
13
13
  s.description = "This gem helps create large (>50) gmail filter chains by writing xml compatible with gmail's \"import/export filters\" feature."
14
14
  s.email = "asf@boinkor.net"
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE.txt",
17
- "README.rdoc"
17
+ "README.md"
18
18
  ]
19
19
  s.files = [
20
20
  ".document",
21
+ ".yardopts",
21
22
  "Gemfile",
22
23
  "Gemfile.lock",
23
24
  "LICENSE.txt",
24
- "README.rdoc",
25
+ "README.md",
25
26
  "Rakefile",
26
27
  "VERSION",
27
28
  "gmail-britta.gemspec",
28
29
  "lib/gmail-britta.rb",
29
- "lib/gmail-britta/delegate.rb",
30
30
  "lib/gmail-britta/filter.rb",
31
+ "lib/gmail-britta/filter_set.rb",
31
32
  "lib/gmail-britta/single_write_accessors.rb",
32
33
  "test/test_gmail-britta.rb"
33
34
  ]
@@ -44,6 +45,8 @@ Gem::Specification.new do |s|
44
45
  s.add_runtime_dependency(%q<haml>, ["~> 3.1.6"])
45
46
  s.add_development_dependency(%q<shoulda>, [">= 0"])
46
47
  s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
48
+ s.add_development_dependency(%q<yard>, ["~> 0.8.3"])
49
+ s.add_development_dependency(%q<redcarpet>, ["~> 2.2.2"])
47
50
  s.add_development_dependency(%q<bundler>, ["~> 1.2.0"])
48
51
  s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
49
52
  s.add_development_dependency(%q<rcov>, [">= 0"])
@@ -53,6 +56,8 @@ Gem::Specification.new do |s|
53
56
  s.add_dependency(%q<haml>, ["~> 3.1.6"])
54
57
  s.add_dependency(%q<shoulda>, [">= 0"])
55
58
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
59
+ s.add_dependency(%q<yard>, ["~> 0.8.3"])
60
+ s.add_dependency(%q<redcarpet>, ["~> 2.2.2"])
56
61
  s.add_dependency(%q<bundler>, ["~> 1.2.0"])
57
62
  s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
58
63
  s.add_dependency(%q<rcov>, [">= 0"])
@@ -63,6 +68,8 @@ Gem::Specification.new do |s|
63
68
  s.add_dependency(%q<haml>, ["~> 3.1.6"])
64
69
  s.add_dependency(%q<shoulda>, [">= 0"])
65
70
  s.add_dependency(%q<rdoc>, ["~> 3.12"])
71
+ s.add_dependency(%q<yard>, ["~> 0.8.3"])
72
+ s.add_dependency(%q<redcarpet>, ["~> 2.2.2"])
66
73
  s.add_dependency(%q<bundler>, ["~> 1.2.0"])
67
74
  s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
68
75
  s.add_dependency(%q<rcov>, [">= 0"])
data/lib/gmail-britta.rb CHANGED
@@ -11,50 +11,22 @@ require 'haml'
11
11
  require 'logger'
12
12
 
13
13
  require 'gmail-britta/single_write_accessors'
14
- require 'gmail-britta/delegate'
14
+ require 'gmail-britta/filter_set'
15
15
  require 'gmail-britta/filter'
16
16
 
17
+ # # A generator DSL for importable gmail filter specifications.
18
+ #
19
+ # This is the main entry point for defining a filter set (multiple filters). See {.filterset} for details.
17
20
  module GmailBritta
18
- class Britta
19
- def initialize(opts={})
20
- @filters = []
21
- @me = opts[:me] || 'me'
22
- @logger = opts[:logger] || allocate_logger
23
- end
24
-
25
- def allocate_logger
26
- logger = Logger.new(STDERR)
27
- logger.level = Logger::WARN
28
- logger
29
- end
30
-
31
- attr_accessor :filters
32
- attr_accessor :me
33
- attr_accessor :logger
34
-
35
- def rules(&block)
36
- GmailBritta::Delegate.new(self, :logger => @logger).perform(&block)
37
- end
38
-
39
- def generate
40
- engine = Haml::Engine.new(<<-ATOM)
41
- !!! XML
42
- %feed{:xmlns => 'http://www.w3.org/2005/Atom', 'xmlns:apps' => 'http://schemas.google.com/apps/2006'}
43
- %title Mail Filters
44
- %id tag:mail.google.com,2008:filters:
45
- %updated #{Time.now.utc.iso8601}
46
- %author
47
- %name Andreas Fuchs
48
- %email asf@boinkor.net
49
- - filters.each do |filter|
50
- != filter.generate_xml
51
- ATOM
52
- engine.render(self)
53
- end
54
- end
55
21
 
22
+ # Create a {FilterSet} and run the filter set definition in the block.
23
+ # This is the main entry point for GmailBritta.
24
+ # @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
+ # @option opts :logger [Logger] (Logger.new()) An initialized logger instance.
26
+ # @yield the filterset definition block. `self` inside the block is the {FilterSet} instance.
27
+ # @return [FilterSet] the constructed filterset
56
28
  def self.filterset(opts={}, &block)
57
- (britta = Britta.new(opts)).rules(&block)
29
+ (britta = FilterSet.new(opts)).rules(&block)
58
30
  britta
59
31
  end
60
32
  end
@@ -1,27 +1,137 @@
1
1
  module GmailBritta
2
+ # This class specifies the behavior of a single filter
3
+ # definition. Create a filter object in the DSL via
4
+ # {FilterSet::Delegator#filter} or use {Filter#also},
5
+ # {Filter#otherwise} or {Filter#archive_unless_directed} to make a
6
+ # new filter based on another one.
7
+ # @todo this probably needs some explanatory docs to make it understandable.
8
+
2
9
  class Filter
3
10
  include SingleWriteAccessors
4
- single_write_accessor :has, 'hasTheWord' do |list|
5
- emit_filter_spec(list)
6
- end
7
- single_write_accessor :has_not, 'doesNotHaveTheWord' do |list|
8
- emit_filter_spec(list)
9
- end
11
+
12
+ # @!group Methods for use in a filter definition block
13
+ # Archive the message.
14
+ # @!macro [new] bool_dsl_method
15
+ # @return [void]
16
+ # @!method $1()
10
17
  single_write_boolean_accessor :archive, 'shouldArchive'
18
+
19
+ # Move the message to the trash.
20
+ # @macro bool_dsl_method
11
21
  single_write_boolean_accessor :delete_it, 'shouldTrash'
22
+
23
+ # Mark the message as read.
24
+ # @macro bool_dsl_method
12
25
  single_write_boolean_accessor :mark_read, 'shouldMarkAsRead'
26
+
27
+ # Mark the message as important.
28
+ # @macro bool_dsl_method
13
29
  single_write_boolean_accessor :mark_important, 'shouldAlwaysMarkAsImportant'
30
+
31
+ # Do not mark the message as important.
32
+ # @macro bool_dsl_method
14
33
  single_write_boolean_accessor :mark_unimportant, 'shouldNeverMarkAsImportant'
34
+
35
+ # Star the message
36
+ # @macro bool_dsl_method
15
37
  single_write_boolean_accessor :star, 'shouldStar'
38
+
39
+ # Never mark the message as spam
40
+ # @macro bool_dsl_method
16
41
  single_write_boolean_accessor :never_spam, 'shouldNeverSpam'
42
+
43
+ # Assign the given label to the message
44
+ # @return [void]
45
+ # @!method label(label)
46
+ # @param [String] label the label to assign the message
17
47
  single_write_accessor :label, 'label'
48
+
49
+ # Forward the message to the given label.
50
+ # @return [void]
51
+ # @!method forward_to(email)
52
+ # @param [String] email an email address to forward the message to
18
53
  single_write_accessor :forward_to, 'forwardTo'
19
54
 
55
+ # @!method has(conditions)
56
+ # @return [void]
57
+ # Defines the positive conditions for the filter to match.
58
+ # @overload has([conditions])
59
+ # Conditions ANDed together that an incoming email must match.
60
+ # @param [Array<conditions>] conditions a list of gmail search terms, all of which must match
61
+ # @overload has({:or => [conditions]})
62
+ # Conditions ORed together for the filter to match
63
+ # @param [{:or => conditions}] conditions a hash of the form `{:or => [condition1, condition2]}` - either of these conditions must match to match the filter.
64
+ single_write_accessor :has, 'hasTheWord' do |list|
65
+ emit_filter_spec(list)
66
+ end
67
+
68
+ # @!method has_not(conditions)
69
+ # @return [void]
70
+ # Defines the negative conditions that must not match for the filter to be allowed to match.
71
+ single_write_accessor :has_not, 'doesNotHaveTheWord' do |list|
72
+ emit_filter_spec(list)
73
+ end
74
+ # @!endgroup
75
+
76
+ # Register and return a new filter that matches only if this
77
+ # filter's conditions (those that are not duplicated on the new
78
+ # filter's `has` clause) do not match.
79
+ # @yield The filter definition block
80
+ # @return [Filter] the new filter
81
+ def otherwise(&block)
82
+ filter = Filter.new(@britta, :log => @log).perform(&block)
83
+ filter.merge_negated_criteria(self)
84
+ filter.log_definition
85
+ filter
86
+ end
87
+
88
+ # Register and return a new filter that matches a message only if
89
+ # this filter's conditions *and* the previous filter's condition
90
+ # match.
91
+ # @yield The filter definition block
92
+ # @return [Filter] the new filter
93
+ def also(&block)
94
+ filter = Filter.new(@britta, :log => @log).perform(&block)
95
+ filter.merge_positive_criteria(self)
96
+ filter.log_definition
97
+ filter
98
+ end
99
+
100
+ # Register (but don't return) a filter that archives the message
101
+ # unless it matches the `:to` email addresses. Optionally, mark
102
+ # the message as read if this filter matches.
103
+ #
104
+ # @note This method returns the previous filter to make it easier to construct filter chains with {#otherwise} and {#also}.
105
+ #
106
+ # @option options [true, false] :mark_read If true, mark the message as read
107
+ # @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}.
108
+ # @return [Filter] the current (not the newly-constructed filter)
109
+ def archive_unless_directed(options={})
110
+ mark_as_read=options[:mark_read]
111
+ tos=(options[:to] || me).to_a
112
+ filter = Filter.new(@britta, :log => @log).perform do
113
+ has_not [{:or => tos.map {|to| "to:#{to}"}}]
114
+ archive
115
+ if mark_as_read
116
+ mark_read
117
+ end
118
+ end
119
+ filter.merge_positive_criteria(self)
120
+ filter.log_definition
121
+ self
122
+ end
123
+
124
+ # Create a new filter object
125
+ # @note Over the lifetime of {GmailBritta}, new {Filter}s usually get created only by the {Delegate}.
126
+ # @param [GmailBritta::Britta] britta the filterset object
127
+ # @option options :log [Logger] a logger for debug messages
20
128
  def initialize(britta, options={})
21
129
  @britta = britta
22
130
  @log = options[:log]
23
131
  end
24
132
 
133
+ # Return the filter's value as XML text.
134
+ # @return [String] the Atom XML representation of this filter
25
135
  def generate_xml
26
136
  engine = Haml::Engine.new(<<-ATOM)
27
137
  %entry
@@ -36,49 +146,19 @@ ATOM
36
146
  engine.render(self)
37
147
  end
38
148
 
39
- def self.emit_filter_spec(filter, infix=' ')
40
- str = ''
41
- case filter
42
- when String
43
- str << filter
44
- when Hash
45
- filter.keys.each do |key|
46
- case key
47
- when :or
48
- str << '('
49
- str << emit_filter_spec(filter[key], ' OR ')
50
- str << ')'
51
- when :not
52
- str << '-('
53
- str << emit_filter_spec(filter[key], ' ')
54
- str << ')'
55
- end
56
- end
57
- when Array
58
- str << filter.map {|elt| emit_filter_spec(elt, ' ')}.join(infix)
59
- end
60
- str
61
- end
62
-
63
- def me
64
- @britta.me
65
- end
66
-
67
- def log_definition
68
- @log.debug "Filter: #{self}"
69
- Filter.single_write_accessors.each do |name|
70
- val = instance_variable_get(Filter.ivar_name(name))
71
- @log.debug " #{name}: #{val}" if val
72
- end
73
- self
74
- end
75
-
149
+ # Evaluate block as a filter definition block and register `self` as a filter on the containing {FilterSet}
150
+ # @note this method gets called by {Delegate#filter} to create and register a new filter object
151
+ # @yield The filter definition. `self` in the block is the new filter object.
152
+ # @api private
153
+ # @return [Filter] the filter that
76
154
  def perform(&block)
77
155
  instance_eval(&block)
78
156
  @britta.filters << self
79
157
  self
80
158
  end
81
159
 
160
+ protected
161
+
82
162
  def merge_negated_criteria(filter)
83
163
  old_has_not = Marshal.load(Marshal.dump((filter.get_has_not || []).reject { |elt|
84
164
  @has.member?(elt)
@@ -103,13 +183,6 @@ ATOM
103
183
  @log.debug(" M: nhn #{@has_not.inspect}")
104
184
  end
105
185
 
106
- def otherwise(&block)
107
- filter = Filter.new(@britta, :log => @log).perform(&block)
108
- filter.merge_negated_criteria(self)
109
- filter.log_definition
110
- filter
111
- end
112
-
113
186
  def merge_positive_criteria(filter)
114
187
  new_has = (@has || []) + (filter.get_has || [])
115
188
  new_has_not = (@has_not || []) + (filter.get_has_not || [])
@@ -117,26 +190,44 @@ ATOM
117
190
  @has_not = new_has_not
118
191
  end
119
192
 
120
- def also(&block)
121
- filter = Filter.new(@britta, :log => @log).perform(&block)
122
- filter.merge_positive_criteria(self)
123
- filter.log_definition
124
- filter
193
+ def self.emit_filter_spec(filter, infix=' ')
194
+ str = ''
195
+ case filter
196
+ when String
197
+ str << filter
198
+ when Hash
199
+ filter.keys.each do |key|
200
+ case key
201
+ when :or
202
+ str << '('
203
+ str << emit_filter_spec(filter[key], ' OR ')
204
+ str << ')'
205
+ when :not
206
+ str << '-('
207
+ str << emit_filter_spec(filter[key], ' ')
208
+ str << ')'
209
+ end
210
+ end
211
+ when Array
212
+ str << filter.map {|elt| emit_filter_spec(elt, ' ')}.join(infix)
213
+ end
214
+ str
125
215
  end
126
216
 
127
- def archive_unless_directed(options={})
128
- mark_as_read=options[:mark_read]
129
- tos=(options[:to] || me).to_a
130
- filter = Filter.new(@britta, :log => @log).perform do
131
- has_not [{:or => tos.map {|to| "to:#{to}"}}]
132
- archive
133
- if mark_as_read
134
- mark_read
135
- end
217
+ # Note a filter definition on the logger.
218
+ # @note for debugging only.
219
+ def log_definition
220
+ @log.debug "Filter: #{self}"
221
+ Filter.single_write_accessors.each do |name|
222
+ val = instance_variable_get(Filter.ivar_name(name))
223
+ @log.debug " #{name}: #{val}" if val
136
224
  end
137
- filter.merge_positive_criteria(self)
138
- filter.log_definition
139
225
  self
140
226
  end
227
+
228
+ # Return the list of emails that the filterset has configured as "me".
229
+ def me
230
+ @britta.me
231
+ end
141
232
  end
142
233
  end
@@ -0,0 +1,82 @@
1
+ module GmailBritta
2
+ class FilterSet
3
+ def initialize(opts={})
4
+ @filters = []
5
+ @me = opts[:me] || 'me'
6
+ @logger = opts[:logger] || allocate_logger
7
+ end
8
+
9
+ # Currently defined filters
10
+ # @see Delegate#filter
11
+ # @see GmailBritta::Filter#otherwise
12
+ # @see GmailBritta::Filter#also
13
+ # @see GmailBritta::Filter#archive_unless_directed
14
+ attr_accessor :filters
15
+
16
+ # The list of emails that belong to the user running this {FilterSet} definition
17
+ # @see GmailBritta.filterset
18
+ attr_accessor :me
19
+
20
+ # The logger currently being used for debug output
21
+ # @see GmailBritta.filterset
22
+ attr_accessor :logger
23
+
24
+ # Run the block that defines the filters in {Delegate}'s `instance_eval`. This method will typically only be called by {GmailBritta.filterset}.
25
+ # @api private
26
+ # @yield the filter definition block in {Delegate}'s instance_eval.
27
+ def rules(&block)
28
+ Delegate.new(self, :logger => @logger).perform(&block)
29
+ end
30
+
31
+ # Generate ATOM XML for the defined filter set and return it as a String.
32
+ # @return [String] the generated XML, ready for importing into Gmail.
33
+ def generate
34
+ engine = Haml::Engine.new(<<-ATOM)
35
+ !!! XML
36
+ %feed{:xmlns => 'http://www.w3.org/2005/Atom', 'xmlns:apps' => 'http://schemas.google.com/apps/2006'}
37
+ %title Mail Filters
38
+ %id tag:mail.google.com,2008:filters:
39
+ %updated #{Time.now.utc.iso8601}
40
+ %author
41
+ %name Andreas Fuchs
42
+ %email asf@boinkor.net
43
+ - filters.each do |filter|
44
+ != filter.generate_xml
45
+ ATOM
46
+ engine.render(self)
47
+ end
48
+
49
+ # A class whose sole purpose it is to be the `self` in a {FilterSet} definition block.
50
+ class Delegate
51
+
52
+ # @api private
53
+ def initialize(britta, options={})
54
+ @britta = britta
55
+ @log = options[:logger]
56
+ @filter = nil
57
+ end
58
+
59
+ # Create, register and return a new {Filter} without any merged conditions
60
+ # @yield [] the {Filter} definition block, with the new {Filter} instance as `self`.
61
+ # @return [Filtere] the new filter.
62
+ def filter(&block)
63
+ GmailBritta::Filter.new(@britta, :log => @log).perform(&block)
64
+ end
65
+
66
+ # Evaluate the {FilterSet} definition block with the {Delegate} object as `self`
67
+ # @api private
68
+ # @note this method will typically only be called by {FilterSet#rules}
69
+ # @yield [ ] that filterset definition block
70
+ def perform(&block)
71
+ instance_eval(&block)
72
+ end
73
+ end
74
+
75
+ private
76
+ def allocate_logger
77
+ logger = Logger.new(STDERR)
78
+ logger.level = Logger::WARN
79
+ logger
80
+ end
81
+ end
82
+ end
@@ -51,6 +51,7 @@ module GmailBritta
51
51
  end
52
52
  end
53
53
 
54
+ # @!visibility private
54
55
  def self.included(base)
55
56
  base.extend(ClassMethods)
56
57
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gmail-britta
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 4
10
- version: 0.1.4
9
+ - 5
10
+ version: 0.1.5
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andreas Fuchs
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-12-22 00:00:00 Z
18
+ date: 2012-12-23 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  version_requirements: &id001 !ruby/object:Gem::Requirement
@@ -64,6 +64,38 @@ dependencies:
64
64
  requirement: *id003
65
65
  - !ruby/object:Gem::Dependency
66
66
  version_requirements: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ~>
70
+ - !ruby/object:Gem::Version
71
+ hash: 57
72
+ segments:
73
+ - 0
74
+ - 8
75
+ - 3
76
+ version: 0.8.3
77
+ prerelease: false
78
+ type: :development
79
+ name: yard
80
+ requirement: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ version_requirements: &id005 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 2
90
+ - 2
91
+ - 2
92
+ version: 2.2.2
93
+ prerelease: false
94
+ type: :development
95
+ name: redcarpet
96
+ requirement: *id005
97
+ - !ruby/object:Gem::Dependency
98
+ version_requirements: &id006 !ruby/object:Gem::Requirement
67
99
  none: false
68
100
  requirements:
69
101
  - - ~>
@@ -77,9 +109,9 @@ dependencies:
77
109
  prerelease: false
78
110
  type: :development
79
111
  name: bundler
80
- requirement: *id004
112
+ requirement: *id006
81
113
  - !ruby/object:Gem::Dependency
82
- version_requirements: &id005 !ruby/object:Gem::Requirement
114
+ version_requirements: &id007 !ruby/object:Gem::Requirement
83
115
  none: false
84
116
  requirements:
85
117
  - - ~>
@@ -93,9 +125,9 @@ dependencies:
93
125
  prerelease: false
94
126
  type: :development
95
127
  name: jeweler
96
- requirement: *id005
128
+ requirement: *id007
97
129
  - !ruby/object:Gem::Dependency
98
- version_requirements: &id006 !ruby/object:Gem::Requirement
130
+ version_requirements: &id008 !ruby/object:Gem::Requirement
99
131
  none: false
100
132
  requirements:
101
133
  - - ">="
@@ -107,9 +139,9 @@ dependencies:
107
139
  prerelease: false
108
140
  type: :development
109
141
  name: rcov
110
- requirement: *id006
142
+ requirement: *id008
111
143
  - !ruby/object:Gem::Dependency
112
- version_requirements: &id007 !ruby/object:Gem::Requirement
144
+ version_requirements: &id009 !ruby/object:Gem::Requirement
113
145
  none: false
114
146
  requirements:
115
147
  - - ">="
@@ -121,9 +153,9 @@ dependencies:
121
153
  prerelease: false
122
154
  type: :development
123
155
  name: minitest
124
- requirement: *id007
156
+ requirement: *id009
125
157
  - !ruby/object:Gem::Dependency
126
- version_requirements: &id008 !ruby/object:Gem::Requirement
158
+ version_requirements: &id010 !ruby/object:Gem::Requirement
127
159
  none: false
128
160
  requirements:
129
161
  - - ">="
@@ -135,7 +167,7 @@ dependencies:
135
167
  prerelease: false
136
168
  type: :development
137
169
  name: nokogiri
138
- requirement: *id008
170
+ requirement: *id010
139
171
  description: This gem helps create large (>50) gmail filter chains by writing xml compatible with gmail's "import/export filters" feature.
140
172
  email: asf@boinkor.net
141
173
  executables: []
@@ -144,19 +176,20 @@ extensions: []
144
176
 
145
177
  extra_rdoc_files:
146
178
  - LICENSE.txt
147
- - README.rdoc
179
+ - README.md
148
180
  files:
149
181
  - .document
182
+ - .yardopts
150
183
  - Gemfile
151
184
  - Gemfile.lock
152
185
  - LICENSE.txt
153
- - README.rdoc
186
+ - README.md
154
187
  - Rakefile
155
188
  - VERSION
156
189
  - gmail-britta.gemspec
157
190
  - lib/gmail-britta.rb
158
- - lib/gmail-britta/delegate.rb
159
191
  - lib/gmail-britta/filter.rb
192
+ - lib/gmail-britta/filter_set.rb
160
193
  - lib/gmail-britta/single_write_accessors.rb
161
194
  - test/test_gmail-britta.rb
162
195
  homepage: http://github.com/antifuchs/gmail-britta
data/README.rdoc DELETED
@@ -1,24 +0,0 @@
1
- = gmail-britta
2
-
3
- This gem assists in writing complex gmail filters. You probably have a
4
- lot of questions, and I'm sorry this README currently answers so few
5
- of them )-:
6
-
7
- A lot of this is very hacky, as it started as a oneoff ruby script -
8
- very sorry for any amateurish code you may encounter. All I can say
9
- is, it works for me [-:
10
-
11
- == Contributing to gmail-britta
12
-
13
- * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
14
- * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
15
- * Fork the project.
16
- * Start a feature/bugfix branch.
17
- * Commit and push until you are happy with your contribution.
18
- * Make sure to add tests for it. (Note that are only very few tests yet. I would totally appreciate if you started fleshing out this suite, but I wouldn't expect you to add much of anything.)
19
- * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
20
-
21
- == Copyright
22
-
23
- Copyright (c) 2012 Andreas Fuchs. See LICENSE.txt for
24
- further details.
@@ -1,17 +0,0 @@
1
- module GmailBritta
2
- class Delegate
3
- def initialize(britta, options={})
4
- @britta = britta
5
- @log = options[:logger]
6
- @filter = nil
7
- end
8
-
9
- def filter(&block)
10
- GmailBritta::Filter.new(@britta, :log => @log).perform(&block)
11
- end
12
-
13
- def perform(&block)
14
- instance_eval(&block)
15
- end
16
- end
17
- end