inbox-sync 0.2.1 → 0.3.0
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.
- data/README.md +67 -3
 - data/lib/inbox-sync/config/filter.rb +41 -0
 - data/lib/inbox-sync/config.rb +18 -0
 - data/lib/inbox-sync/filter_actions.rb +86 -0
 - data/lib/inbox-sync/mail_item.rb +12 -2
 - data/lib/inbox-sync/sync.rb +19 -6
 - data/lib/inbox-sync/version.rb +1 -1
 - data/test/config/filter_test.rb +31 -0
 - data/test/config_test.rb +8 -1
 - data/test/filter_actions_test.rb +70 -0
 - metadata +11 -5
 
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,6 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # InboxSync
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            Move messages from one inbox to another.  Useful when server-side email forwarding is not an option.   
     | 
| 
      
 3 
     | 
    
         
            +
            Move messages from one inbox to another.  Useful when server-side email forwarding is not an option.  Can apply filters to messages as they are being moved.  Run on-demand, on a schedule, or as a daemon.
         
     | 
| 
       4 
4 
     | 
    
         | 
| 
       5 
5 
     | 
    
         
             
            ## Installation
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
         @@ -20,7 +20,7 @@ Or install it yourself as: 
     | 
|
| 
       20 
20 
     | 
    
         | 
| 
       21 
21 
     | 
    
         
             
            InboxSync uses IMAP to query a source inbox, process its messages, append them to a destination inbox, and archive them on the source.  It logs each step in the process and will send notification emails when something goes wrong.
         
     | 
| 
       22 
22 
     | 
    
         | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
      
 23 
     | 
    
         
            +
            InboxSync provides a framework for defining destination filters for post-sync mail processing (ie moving/archiving, copying/labeling, deletion, etc).
         
     | 
| 
       24 
24 
     | 
    
         | 
| 
       25 
25 
     | 
    
         
             
            InboxSync provides a basic ruby runner class to handle polling the source on an interval and running the configured sync(s).  You can call it in any number of ways: in a script, from a cron, as a daemon, or as part of a larger system.
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
         @@ -158,7 +158,71 @@ The runner traps `SIGINT` and `SIGQUIT` and will shutdown nicely once any in-pro 
     | 
|
| 
       158 
158 
     | 
    
         | 
| 
       159 
159 
     | 
    
         
             
            ## Filter Framework
         
     | 
| 
       160 
160 
     | 
    
         | 
| 
       161 
     | 
    
         
            -
             
     | 
| 
      
 161 
     | 
    
         
            +
            You can configure filters for your syncs.  Filters are applied to destination messages after they have been appended to the inbox.
         
     | 
| 
      
 162 
     | 
    
         
            +
             
     | 
| 
      
 163 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 164 
     | 
    
         
            +
            sync = InboxSync.new.configure do
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
              # conditions to match on
         
     | 
| 
      
 167 
     | 
    
         
            +
              filter(:subject => contains('hi there')) do
         
     | 
| 
      
 168 
     | 
    
         
            +
                # actions to perform
         
     | 
| 
      
 169 
     | 
    
         
            +
                copy_to 'Some-Folder'
         
     | 
| 
      
 170 
     | 
    
         
            +
              end
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
            end
         
     | 
| 
      
 173 
     | 
    
         
            +
            ```
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
            ### Conditions
         
     | 
| 
      
 176 
     | 
    
         
            +
             
     | 
| 
      
 177 
     | 
    
         
            +
            The first step in defining a filter is specifying the match conditions.  You can filter on any attribute of the message (`Mail::Message`).  The filter conditions are specified as a hash, where the keys are the attributes to match on and the values are what to match with.
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
            The default comparison is equals (`==`).  You have a few helpers for other comparisons at your disposal as well:
         
     | 
| 
      
 180 
     | 
    
         
            +
             
     | 
| 
      
 181 
     | 
    
         
            +
            * `contains`: converts to `/.*#{value}.*/` and matches.  aliased as `like` and `includes`.
         
     | 
| 
      
 182 
     | 
    
         
            +
            * `starts_with`: converts your value to `/\A#{value}.*/` and matches.  aliased as `sw`.
         
     | 
| 
      
 183 
     | 
    
         
            +
            * `ends_with`: converts your value to `/.*#{value}\Z/` and matches.  aliased as `ew`.
         
     | 
| 
      
 184 
     | 
    
         
            +
            * pass a custom regex: it will be matched
         
     | 
| 
      
 185 
     | 
    
         
            +
             
     | 
| 
      
 186 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 187 
     | 
    
         
            +
            sync = InboxSync.new.configure do
         
     | 
| 
      
 188 
     | 
    
         
            +
             
     | 
| 
      
 189 
     | 
    
         
            +
              filter(:subject => 'hi there you')     { ... }
         
     | 
| 
      
 190 
     | 
    
         
            +
              filter(:subject => contains('there'))  { ... }
         
     | 
| 
      
 191 
     | 
    
         
            +
              filter(:subject => starts_with('hi')   { ... }
         
     | 
| 
      
 192 
     | 
    
         
            +
              filter(:subject => ends_with('you'))   { ... }
         
     | 
| 
      
 193 
     | 
    
         
            +
              filter(:subject => /\Ahi\s+.+\s+you\Z/ { ... }
         
     | 
| 
      
 194 
     | 
    
         
            +
             
     | 
| 
      
 195 
     | 
    
         
            +
            end
         
     | 
| 
      
 196 
     | 
    
         
            +
            ```
         
     | 
| 
      
 197 
     | 
    
         
            +
             
     | 
| 
      
 198 
     | 
    
         
            +
            ### Actions
         
     | 
| 
      
 199 
     | 
    
         
            +
             
     | 
| 
      
 200 
     | 
    
         
            +
            The second step in defining a filter is what to do with a message if it matches.  InboxSync provides a set of actions that can be performed on a message.
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
      
 202 
     | 
    
         
            +
            * `copy_to`: copies the message to a given folder.  will create the folder if necessary.  aliased as `label`.
         
     | 
| 
      
 203 
     | 
    
         
            +
            * `move_to`: moves the message to a given folder.  will create the folder if necessary.  aliased as `archive_to`.
         
     | 
| 
      
 204 
     | 
    
         
            +
            * `mark_read`: marks the message as read (flag :Seen)
         
     | 
| 
      
 205 
     | 
    
         
            +
            * `delete`: deletes the message (flag :Deleted)
         
     | 
| 
      
 206 
     | 
    
         
            +
            * `flag`: apply a custom flag
         
     | 
| 
      
 207 
     | 
    
         
            +
             
     | 
| 
      
 208 
     | 
    
         
            +
            Actions are specified using a block.  If a message matches the filter conditions, the filters actions will be applied to the message on the destination.  In the case multiplie filters match the message, actions are aggregated and applied once after all filters have been processed.
         
     | 
| 
      
 209 
     | 
    
         
            +
             
     | 
| 
      
 210 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 211 
     | 
    
         
            +
            sync = InboxSync.new.configure do
         
     | 
| 
      
 212 
     | 
    
         
            +
             
     | 
| 
      
 213 
     | 
    
         
            +
              filter(...) { copy_to 'Something' }
         
     | 
| 
      
 214 
     | 
    
         
            +
              filter(...) { move_to 'Somewhere' }
         
     | 
| 
      
 215 
     | 
    
         
            +
              filter(...) { mark_read; archive }
         
     | 
| 
      
 216 
     | 
    
         
            +
             
     | 
| 
      
 217 
     | 
    
         
            +
            end
         
     | 
| 
      
 218 
     | 
    
         
            +
            ```
         
     | 
| 
      
 219 
     | 
    
         
            +
             
     | 
| 
      
 220 
     | 
    
         
            +
            Actions are applied according to precedence rules.  They go something like this:
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
            * flags first - they will carry over as messages are copied/moved.
         
     | 
| 
      
 223 
     | 
    
         
            +
            * copies/moves next - moves are just a macro for copy-then-delete
         
     | 
| 
      
 224 
     | 
    
         
            +
             
     | 
| 
      
 225 
     | 
    
         
            +
            This order ensures the message is available for all actions needed.
         
     | 
| 
       162 
226 
     | 
    
         | 
| 
       163 
227 
     | 
    
         
             
            ## Error Handling
         
     | 
| 
       164 
228 
     | 
    
         | 
| 
         @@ -0,0 +1,41 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module InboxSync
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Config; end
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              class Config::Filter
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                attr_reader :conditions, :actions
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def initialize(conditions, &actions)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @actions = actions
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  # make sure all match conditions are regexps
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @conditions = conditions.keys.inject({}) do |processed, key|
         
     | 
| 
      
 13 
     | 
    
         
            +
                    val = conditions[key]
         
     | 
| 
      
 14 
     | 
    
         
            +
                    processed[key] = val.kind_of?(Regexp) ? val : /#{val.to_s}/
         
     | 
| 
      
 15 
     | 
    
         
            +
                    processed
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def match?(message)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @conditions.keys.inject(true) do |result, key|
         
     | 
| 
      
 21 
     | 
    
         
            +
                    result && value_matches?(message.send(key), @conditions[key])
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                protected
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def value_matches?(value, regexp)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  if value.respond_to?(:inject)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    # this is a collection, match if any one item matches
         
     | 
| 
      
 30 
     | 
    
         
            +
                    value.inject(false) do |result, item|
         
     | 
| 
      
 31 
     | 
    
         
            +
                      result || !!(item.to_s =~ regexp)
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
                  else
         
     | 
| 
      
 34 
     | 
    
         
            +
                    # single value match
         
     | 
| 
      
 35 
     | 
    
         
            +
                    !!(value.to_s =~ regexp)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/inbox-sync/config.rb
    CHANGED
    
    | 
         @@ -2,6 +2,7 @@ require 'logger' 
     | 
|
| 
       2 
2 
     | 
    
         
             
            require 'ns-options'
         
     | 
| 
       3 
3 
     | 
    
         
             
            require 'inbox-sync/config/imap_config'
         
     | 
| 
       4 
4 
     | 
    
         
             
            require 'inbox-sync/config/smtp_config'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'inbox-sync/config/filter'
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
       6 
7 
     | 
    
         
             
            module InboxSync
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
         @@ -14,6 +15,11 @@ module InboxSync 
     | 
|
| 
       14 
15 
     | 
    
         | 
| 
       15 
16 
     | 
    
         
             
                opt :archive_folder, :default => 'Archived'
         
     | 
| 
       16 
17 
     | 
    
         
             
                opt :logger, Logger, :required => true, :default => STDOUT
         
     | 
| 
      
 18 
     | 
    
         
            +
                opt :filters, :default => [], :required => true
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def filter(*args, &block)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  filters << Filter.new(*args, &block)
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
       17 
23 
     | 
    
         | 
| 
       18 
24 
     | 
    
         
             
                def validate!
         
     | 
| 
       19 
25 
     | 
    
         
             
                  if !required_set?
         
     | 
| 
         @@ -25,6 +31,18 @@ module InboxSync 
     | 
|
| 
       25 
31 
     | 
    
         
             
                  notify.validate!
         
     | 
| 
       26 
32 
     | 
    
         
             
                end
         
     | 
| 
       27 
33 
     | 
    
         | 
| 
      
 34 
     | 
    
         
            +
                protected
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                def contains(value);    /.*#{value}.*/; end
         
     | 
| 
      
 37 
     | 
    
         
            +
                def starts_with(value); /\A#{value}.*/; end
         
     | 
| 
      
 38 
     | 
    
         
            +
                def ends_with(value);   /.*#{value}\Z/; end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                alias_method :like, :contains
         
     | 
| 
      
 41 
     | 
    
         
            +
                alias_method :includes, :contains
         
     | 
| 
      
 42 
     | 
    
         
            +
                alias_method :inc, :includes
         
     | 
| 
      
 43 
     | 
    
         
            +
                alias_method :sw,  :starts_with
         
     | 
| 
      
 44 
     | 
    
         
            +
                alias_method :ew,  :ends_with
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
       28 
46 
     | 
    
         
             
              end
         
     | 
| 
       29 
47 
     | 
    
         | 
| 
       30 
48 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,86 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module InboxSync
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              class FilterActions
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                attr_reader :message
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(message)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @message = message
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @copies  = @flags = []
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def copies; @copies.uniq; end
         
     | 
| 
      
 13 
     | 
    
         
            +
                def flags;  @flags.uniq;  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def copy_to(*folders)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @copies += args_collection(folders)
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
                alias_method :label, :copy_to
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def move_to(*folders)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  copy_to *folders
         
     | 
| 
      
 22 
     | 
    
         
            +
                  delete
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
                alias_method :archive_to, :move_to
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def flag(*flags)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  @flags += args_collection(flags).map{|f| f.to_sym}
         
     | 
| 
      
 28 
     | 
    
         
            +
                end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                def mark_read
         
     | 
| 
      
 31 
     | 
    
         
            +
                  flag(:Seen)
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def delete
         
     | 
| 
      
 35 
     | 
    
         
            +
                  flag(:Deleted)
         
     | 
| 
      
 36 
     | 
    
         
            +
                end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                def match!(filters)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  filters.each do |filter|
         
     | 
| 
      
 40 
     | 
    
         
            +
                    instance_eval(&filter.actions) if filter.match?(@message)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def apply!(imap, uid)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  apply_flags(imap, uid)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  apply_copies(imap, uid)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  # force make the dest message unread if not explicitly marked :Seen
         
     | 
| 
      
 49 
     | 
    
         
            +
                  if !flags.include?(:Seen)
         
     | 
| 
      
 50 
     | 
    
         
            +
                    imap.uid_store(uid, "-FLAGS", [:Seen])
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                protected
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                def apply_flags(imap, uid)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  if !flags.empty?
         
     | 
| 
      
 58 
     | 
    
         
            +
                    imap.uid_store(uid, "+FLAGS", flags)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  end
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                def apply_copies(imap, uid)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  copies.each do |folder|
         
     | 
| 
      
 64 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 65 
     | 
    
         
            +
                      imap.uid_copy(uid, folder)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    rescue Net::IMAP::NoResponseError
         
     | 
| 
      
 67 
     | 
    
         
            +
                      imap.create(folder)
         
     | 
| 
      
 68 
     | 
    
         
            +
                      retry
         
     | 
| 
      
 69 
     | 
    
         
            +
                    end
         
     | 
| 
      
 70 
     | 
    
         
            +
                  end
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                private
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                def args_collection(args)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  args.
         
     | 
| 
      
 77 
     | 
    
         
            +
                    flatten.
         
     | 
| 
      
 78 
     | 
    
         
            +
                    compact.
         
     | 
| 
      
 79 
     | 
    
         
            +
                    map {|f| f.to_s}.
         
     | 
| 
      
 80 
     | 
    
         
            +
                    reject {|f| f.empty?}.
         
     | 
| 
      
 81 
     | 
    
         
            +
                    uniq
         
     | 
| 
      
 82 
     | 
    
         
            +
                end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
              end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/inbox-sync/mail_item.rb
    CHANGED
    
    | 
         @@ -25,7 +25,13 @@ module InboxSync 
     | 
|
| 
       25 
25 
     | 
    
         
             
                end
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
       27 
27 
     | 
    
         
             
                def meta
         
     | 
| 
       28 
     | 
    
         
            -
                  @meta ||=  
     | 
| 
      
 28 
     | 
    
         
            +
                  @meta ||= begin
         
     | 
| 
      
 29 
     | 
    
         
            +
                    fetch_data = @imap.uid_fetch(self.uid, ['RFC822', 'INTERNALDATE'])
         
     | 
| 
      
 30 
     | 
    
         
            +
                    if fetch_data.nil? || fetch_data.empty?
         
     | 
| 
      
 31 
     | 
    
         
            +
                      raise "error fetching data for uid '#{self.uid}'"
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
                    fetch_data.first
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
       29 
35 
     | 
    
         
             
                end
         
     | 
| 
       30 
36 
     | 
    
         | 
| 
       31 
37 
     | 
    
         
             
                def rfc822
         
     | 
| 
         @@ -69,7 +75,11 @@ module InboxSync 
     | 
|
| 
       69 
75 
     | 
    
         
             
                private
         
     | 
| 
       70 
76 
     | 
    
         | 
| 
       71 
77 
     | 
    
         
             
                def time_s(datetime)
         
     | 
| 
       72 
     | 
    
         
            -
                  datetime.strftime 
     | 
| 
      
 78 
     | 
    
         
            +
                  if datetime && datetime.respond_to?(:strftime)
         
     | 
| 
      
 79 
     | 
    
         
            +
                    datetime.strftime("%a %b %-d %Y, %I:%M %p")
         
     | 
| 
      
 80 
     | 
    
         
            +
                  else
         
     | 
| 
      
 81 
     | 
    
         
            +
                    datetime
         
     | 
| 
      
 82 
     | 
    
         
            +
                  end
         
     | 
| 
       73 
83 
     | 
    
         
             
                end
         
     | 
| 
       74 
84 
     | 
    
         | 
| 
       75 
85 
     | 
    
         
             
                def copy_mail_item(item)
         
     | 
    
        data/lib/inbox-sync/sync.rb
    CHANGED
    
    | 
         @@ -2,6 +2,7 @@ require 'net/imap' 
     | 
|
| 
       2 
2 
     | 
    
         
             
            require 'net/smtp'
         
     | 
| 
       3 
3 
     | 
    
         | 
| 
       4 
4 
     | 
    
         
             
            require 'inbox-sync/config'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'inbox-sync/filter_actions'
         
     | 
| 
       5 
6 
     | 
    
         
             
            require 'inbox-sync/notice/sync_mail_item_error'
         
     | 
| 
       6 
7 
     | 
    
         | 
| 
       7 
8 
     | 
    
         
             
            module InboxSync
         
     | 
| 
         @@ -56,9 +57,10 @@ module InboxSync 
     | 
|
| 
       56 
57 
     | 
    
         
             
                  return if runner && runner.shutdown?
         
     | 
| 
       57 
58 
     | 
    
         
             
                  each_source_mail_item(runner) do |mail_item|
         
     | 
| 
       58 
59 
     | 
    
         
             
                    begin
         
     | 
| 
      
 60 
     | 
    
         
            +
                      logger.debug "** #{mail_item.inspect}"
         
     | 
| 
       59 
61 
     | 
    
         
             
                      response = send_to_dest(mail_item)
         
     | 
| 
       60 
62 
     | 
    
         
             
                      dest_uid = parse_append_response_uid(response)
         
     | 
| 
       61 
     | 
    
         
            -
                       
     | 
| 
      
 63 
     | 
    
         
            +
                      apply_dest_filters(dest_uid)
         
     | 
| 
       62 
64 
     | 
    
         
             
                    rescue Exception => err
         
     | 
| 
       63 
65 
     | 
    
         
             
                      log_error(err)
         
     | 
| 
       64 
66 
     | 
    
         
             
                      notify(Notice::SyncMailItemError.new(@notify_smtp, @config.notify, {
         
     | 
| 
         @@ -106,7 +108,6 @@ module InboxSync 
     | 
|
| 
       106 
108 
     | 
    
         
             
                      logger.info "* the runner has been shutdown - aborting the sync"
         
     | 
| 
       107 
109 
     | 
    
         
             
                      break
         
     | 
| 
       108 
110 
     | 
    
         
             
                    end
         
     | 
| 
       109 
     | 
    
         
            -
                    logger.debug "** #{mail_item.inspect}"
         
     | 
| 
       110 
111 
     | 
    
         
             
                    yield mail_item
         
     | 
| 
       111 
112 
     | 
    
         
             
                  end
         
     | 
| 
       112 
113 
     | 
    
         
             
                  items = nil
         
     | 
| 
         @@ -143,6 +144,21 @@ module InboxSync 
     | 
|
| 
       143 
144 
     | 
    
         
             
                  end
         
     | 
| 
       144 
145 
     | 
    
         
             
                end
         
     | 
| 
       145 
146 
     | 
    
         | 
| 
      
 147 
     | 
    
         
            +
                def apply_dest_filters(dest_uid)
         
     | 
| 
      
 148 
     | 
    
         
            +
                  logger.info "** applying filters for dest uid: #{dest_uid.inspect}"
         
     | 
| 
      
 149 
     | 
    
         
            +
                  using_dest_imap do |imap|
         
     | 
| 
      
 150 
     | 
    
         
            +
                    dest_mail_item = MailItem.new(imap, dest_uid)
         
     | 
| 
      
 151 
     | 
    
         
            +
                    logger.debug "** #{dest_mail_item.inspect}"
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                    actions = FilterActions.new(dest_mail_item.message)
         
     | 
| 
      
 154 
     | 
    
         
            +
                    actions.match!(@config.filters)
         
     | 
| 
      
 155 
     | 
    
         
            +
                    logger.debug "** flag as: #{actions.flags.inspect}"
         
     | 
| 
      
 156 
     | 
    
         
            +
                    logger.debug "** copy to: #{actions.copies.inspect}"
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
                    actions.apply!(imap, dest_uid)
         
     | 
| 
      
 159 
     | 
    
         
            +
                  end
         
     | 
| 
      
 160 
     | 
    
         
            +
                end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
       146 
162 
     | 
    
         
             
                def archive_on_source(mail_item)
         
     | 
| 
       147 
163 
     | 
    
         
             
                  folder = @config.archive_folder
         
     | 
| 
       148 
164 
     | 
    
         
             
                  if !folder.nil? && !folder.empty?
         
     | 
| 
         @@ -164,10 +180,7 @@ module InboxSync 
     | 
|
| 
       164 
180 
     | 
    
         
             
                  end
         
     | 
| 
       165 
181 
     | 
    
         | 
| 
       166 
182 
     | 
    
         
             
                  mark_as_deleted(@source_imap, mail_item.uid)
         
     | 
| 
       167 
     | 
    
         
            -
             
     | 
| 
       168 
183 
     | 
    
         
             
                  expunge_imap(@source_imap, @config.source)
         
     | 
| 
       169 
     | 
    
         
            -
             
     | 
| 
       170 
     | 
    
         
            -
                  @source_imap.expunge
         
     | 
| 
       171 
184 
     | 
    
         
             
                end
         
     | 
| 
       172 
185 
     | 
    
         | 
| 
       173 
186 
     | 
    
         
             
                def using_dest_imap
         
     | 
| 
         @@ -237,7 +250,7 @@ module InboxSync 
     | 
|
| 
       237 
250 
     | 
    
         
             
                # #<struct Net::IMAP::TaggedResponse tag="RUBY0012", name="OK", data=#<struct Net::IMAP::ResponseText code=#<struct Net::IMAP::ResponseCode name="APPENDUID", data="6 9">, text=" (Success)">, raw_data="RUBY0012 OK [APPENDUID 6 9] (Success)\r\n">
         
     | 
| 
       238 
251 
     | 
    
         
             
                # (here '9' is the UID)
         
     | 
| 
       239 
252 
     | 
    
         
             
                def parse_append_response_uid(response)
         
     | 
| 
       240 
     | 
    
         
            -
                  response.data.code.data.split(/\s+/).last
         
     | 
| 
      
 253 
     | 
    
         
            +
                  response.data.code.data.split(/\s+/).last.to_i
         
     | 
| 
       241 
254 
     | 
    
         
             
                end
         
     | 
| 
       242 
255 
     | 
    
         | 
| 
       243 
256 
     | 
    
         
             
                def mark_as_seen(imap, uid)
         
     | 
    
        data/lib/inbox-sync/version.rb
    CHANGED
    
    
| 
         @@ -0,0 +1,31 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'assert'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'inbox-sync/config/filter'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module InboxSync
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              class ConfigFilterTests < Assert::Context
         
     | 
| 
      
 7 
     | 
    
         
            +
                before do
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @filter = InboxSync::Config::Filter.new({
         
     | 
| 
      
 9 
     | 
    
         
            +
                    :subject => 'test',
         
     | 
| 
      
 10 
     | 
    
         
            +
                    :from => /kellyredding.com\Z/
         
     | 
| 
      
 11 
     | 
    
         
            +
                  })
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
                subject { @filter }
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                should have_reader :conditions, :actions
         
     | 
| 
      
 16 
     | 
    
         
            +
                should have_instance_method :match?
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                should "convert condition values to regexs if not" do
         
     | 
| 
      
 19 
     | 
    
         
            +
                  assert_kind_of Hash, subject.conditions
         
     | 
| 
      
 20 
     | 
    
         
            +
                  subject.conditions.each do |k,v|
         
     | 
| 
      
 21 
     | 
    
         
            +
                    assert_kind_of Regexp, v
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                should "know if it matches a message" do
         
     | 
| 
      
 26 
     | 
    
         
            +
                  assert subject.match?(test_mail_item.message)
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            end
         
     | 
    
        data/test/config_test.rb
    CHANGED
    
    | 
         @@ -37,7 +37,14 @@ module InboxSync 
     | 
|
| 
       37 
37 
     | 
    
         
             
                  :default => STDOUT
         
     | 
| 
       38 
38 
     | 
    
         
             
                }
         
     | 
| 
       39 
39 
     | 
    
         | 
| 
       40 
     | 
    
         
            -
                should  
     | 
| 
      
 40 
     | 
    
         
            +
                should have_option :filters, {
         
     | 
| 
      
 41 
     | 
    
         
            +
                  :default => [],
         
     | 
| 
      
 42 
     | 
    
         
            +
                  :required => true
         
     | 
| 
      
 43 
     | 
    
         
            +
                }
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                should have_instance_methods :validate!, :filter
         
     | 
| 
      
 46 
     | 
    
         
            +
                should have_instance_methods :contains, :like, :includes, :inc
         
     | 
| 
      
 47 
     | 
    
         
            +
                should have_instance_methods :starts_with, :ends_with, :sw, :ew
         
     | 
| 
       41 
48 
     | 
    
         | 
| 
       42 
49 
     | 
    
         
             
                should "complain if missing :source config" do
         
     | 
| 
       43 
50 
     | 
    
         
             
                  assert_raises ArgumentError do
         
     | 
| 
         @@ -0,0 +1,70 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'assert'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'inbox-sync/filter_actions'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module InboxSync
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              class FilterActionsTests < Assert::Context
         
     | 
| 
      
 7 
     | 
    
         
            +
                before do
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @message = test_mail_item.message
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @filter_actions = InboxSync::FilterActions.new(@message)
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
                subject { @filter_actions }
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                should have_reader :message
         
     | 
| 
      
 14 
     | 
    
         
            +
                should have_instance_methods :copies, :flags
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                should have_instance_methods :copy_to, :label
         
     | 
| 
      
 17 
     | 
    
         
            +
                should have_instance_methods :move_to, :archive_to
         
     | 
| 
      
 18 
     | 
    
         
            +
                should have_instance_methods :flag, :mark_read, :delete
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                should have_instance_methods :match!, :apply!
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                should "have no marks or copies and not be deleted by default" do
         
     | 
| 
      
 23 
     | 
    
         
            +
                  assert_empty subject.copies
         
     | 
| 
      
 24 
     | 
    
         
            +
                  assert_empty subject.flags
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                should "handle copy actions" do
         
     | 
| 
      
 28 
     | 
    
         
            +
                  subject.copy_to 'somewhere'
         
     | 
| 
      
 29 
     | 
    
         
            +
                  subject.label 'something'
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  assert_equal ['somewhere', 'something'], subject.copies
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                should "only show valid, uniq copies when reading" do
         
     | 
| 
      
 35 
     | 
    
         
            +
                  subject.copy_to 'valid'
         
     | 
| 
      
 36 
     | 
    
         
            +
                  subject.copy_to
         
     | 
| 
      
 37 
     | 
    
         
            +
                  subject.copy_to nil
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  assert_equal ['valid'], subject.copies
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                should "handle move actions as a copy and archive" do
         
     | 
| 
      
 43 
     | 
    
         
            +
                  subject.move_to 'somewhere'
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  assert_equal ['somewhere'], subject.copies
         
     | 
| 
      
 46 
     | 
    
         
            +
                  assert_equal [:Deleted], subject.flags
         
     | 
| 
      
 47 
     | 
    
         
            +
                end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                should "handle marking as read" do
         
     | 
| 
      
 50 
     | 
    
         
            +
                  subject.mark_read
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  assert_equal [:Seen], subject.flags
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
                should "handle delete actions" do
         
     | 
| 
      
 56 
     | 
    
         
            +
                  subject.delete
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                  assert_equal [:Deleted], subject.flags
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                should "handle flag actions" do
         
     | 
| 
      
 62 
     | 
    
         
            +
                  subject.flag :A
         
     | 
| 
      
 63 
     | 
    
         
            +
                  subject.flag :B, [:C]
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                  assert_equal [:A, :B, :C], subject.flags
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
              end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
            end
         
     | 
    
        metadata
    CHANGED
    
    | 
         @@ -1,13 +1,13 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification 
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: inbox-sync
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version 
         
     | 
| 
       4 
     | 
    
         
            -
              hash:  
     | 
| 
      
 4 
     | 
    
         
            +
              hash: 19
         
     | 
| 
       5 
5 
     | 
    
         
             
              prerelease: 
         
     | 
| 
       6 
6 
     | 
    
         
             
              segments: 
         
     | 
| 
       7 
7 
     | 
    
         
             
              - 0
         
     | 
| 
       8 
     | 
    
         
            -
              -  
     | 
| 
       9 
     | 
    
         
            -
              -  
     | 
| 
       10 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 8 
     | 
    
         
            +
              - 3
         
     | 
| 
      
 9 
     | 
    
         
            +
              - 0
         
     | 
| 
      
 10 
     | 
    
         
            +
              version: 0.3.0
         
     | 
| 
       11 
11 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       12 
12 
     | 
    
         
             
            authors: 
         
     | 
| 
       13 
13 
     | 
    
         
             
            - Kelly Redding
         
     | 
| 
         @@ -15,7 +15,7 @@ autorequire: 
     | 
|
| 
       15 
15 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       16 
16 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       17 
17 
     | 
    
         | 
| 
       18 
     | 
    
         
            -
            date: 2012-06- 
     | 
| 
      
 18 
     | 
    
         
            +
            date: 2012-06-15 00:00:00 Z
         
     | 
| 
       19 
19 
     | 
    
         
             
            dependencies: 
         
     | 
| 
       20 
20 
     | 
    
         
             
            - !ruby/object:Gem::Dependency 
         
     | 
| 
       21 
21 
     | 
    
         
             
              type: :development
         
     | 
| 
         @@ -81,8 +81,10 @@ files: 
     | 
|
| 
       81 
81 
     | 
    
         
             
            - lib/inbox-sync.rb
         
     | 
| 
       82 
82 
     | 
    
         
             
            - lib/inbox-sync/config.rb
         
     | 
| 
       83 
83 
     | 
    
         
             
            - lib/inbox-sync/config/credentials.rb
         
     | 
| 
      
 84 
     | 
    
         
            +
            - lib/inbox-sync/config/filter.rb
         
     | 
| 
       84 
85 
     | 
    
         
             
            - lib/inbox-sync/config/imap_config.rb
         
     | 
| 
       85 
86 
     | 
    
         
             
            - lib/inbox-sync/config/smtp_config.rb
         
     | 
| 
      
 87 
     | 
    
         
            +
            - lib/inbox-sync/filter_actions.rb
         
     | 
| 
       86 
88 
     | 
    
         
             
            - lib/inbox-sync/mail_item.rb
         
     | 
| 
       87 
89 
     | 
    
         
             
            - lib/inbox-sync/notice/base.rb
         
     | 
| 
       88 
90 
     | 
    
         
             
            - lib/inbox-sync/notice/run_sync_error.rb
         
     | 
| 
         @@ -91,7 +93,9 @@ files: 
     | 
|
| 
       91 
93 
     | 
    
         
             
            - lib/inbox-sync/sync.rb
         
     | 
| 
       92 
94 
     | 
    
         
             
            - lib/inbox-sync/version.rb
         
     | 
| 
       93 
95 
     | 
    
         
             
            - log/.gitkeep
         
     | 
| 
      
 96 
     | 
    
         
            +
            - test/config/filter_test.rb
         
     | 
| 
       94 
97 
     | 
    
         
             
            - test/config_test.rb
         
     | 
| 
      
 98 
     | 
    
         
            +
            - test/filter_actions_test.rb
         
     | 
| 
       95 
99 
     | 
    
         
             
            - test/helper.rb
         
     | 
| 
       96 
100 
     | 
    
         
             
            - test/irb.rb
         
     | 
| 
       97 
101 
     | 
    
         
             
            - test/mail_item_test.rb
         
     | 
| 
         @@ -132,7 +136,9 @@ signing_key: 
     | 
|
| 
       132 
136 
     | 
    
         
             
            specification_version: 3
         
     | 
| 
       133 
137 
     | 
    
         
             
            summary: Move messages from one inbox to another
         
     | 
| 
       134 
138 
     | 
    
         
             
            test_files: 
         
     | 
| 
      
 139 
     | 
    
         
            +
            - test/config/filter_test.rb
         
     | 
| 
       135 
140 
     | 
    
         
             
            - test/config_test.rb
         
     | 
| 
      
 141 
     | 
    
         
            +
            - test/filter_actions_test.rb
         
     | 
| 
       136 
142 
     | 
    
         
             
            - test/helper.rb
         
     | 
| 
       137 
143 
     | 
    
         
             
            - test/irb.rb
         
     | 
| 
       138 
144 
     | 
    
         
             
            - test/mail_item_test.rb
         
     |