show progress in sup-sync
[sup:mainline.git] / lib / sup / poll.rb
1 require 'thread'
2
3 module Redwood
4
5 class PollManager
6   include Singleton
7
8   HookManager.register "before-add-message", <<EOS
9 Executes immediately before a message is added to the index.
10 Variables:
11   message: the new message
12 EOS
13
14   HookManager.register "before-poll", <<EOS
15 Executes immediately before a poll for new messages commences.
16 No variables.
17 EOS
18
19   HookManager.register "after-poll", <<EOS
20 Executes immediately after a poll for new messages completes.
21 Variables:
22                    num: the total number of new messages added in this poll
23              num_inbox: the number of new messages added in this poll which
24                         appear in the inbox (i.e. were not auto-archived).
25 num_inbox_total_unread: the total number of unread messages in the inbox
26          from_and_subj: an array of (from email address, subject) pairs
27    from_and_subj_inbox: an array of (from email address, subject) pairs for
28                         only those messages appearing in the inbox
29 EOS
30
31   def initialize
32     @delay = $config[:poll_interval] || 300
33     @mutex = Mutex.new
34     @thread = nil
35     @last_poll = nil
36     @polling = false
37     @poll_sources = nil
38     @mode = nil
39     @should_clear_running_totals = false
40     clear_running_totals # defines @running_totals
41     UpdateManager.register self
42   end
43
44   def poll_with_sources
45     @mode ||= PollMode.new
46     HookManager.run "before-poll"
47
48     BufferManager.flash "Polling for new messages..."
49     num, numi, from_and_subj, from_and_subj_inbox, loaded_labels = @mode.poll
50     clear_running_totals if @should_clear_running_totals
51     @running_totals[:num] += num
52     @running_totals[:numi] += numi
53     @running_totals[:loaded_labels] += loaded_labels || []
54     if @running_totals[:num] > 0
55       BufferManager.flash "Loaded #{@running_totals[:num].pluralize 'new message'}, #{@running_totals[:numi]} to inbox. Labels: #{@running_totals[:loaded_labels].map{|l| l.to_s}.join(', ')}"
56     else
57       BufferManager.flash "No new messages." 
58     end
59
60     HookManager.run "after-poll", :num => num, :num_inbox => numi, :from_and_subj => from_and_subj, :from_and_subj_inbox => from_and_subj_inbox, :num_inbox_total_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] }
61
62   end
63
64   def poll
65     return if @polling
66     @polling = true
67     @poll_sources = SourceManager.usual_sources
68     num, numi = poll_with_sources
69     @polling = false
70     [num, numi]
71   end
72
73   def poll_unusual
74     return if @polling
75     @polling = true
76     @poll_sources = SourceManager.unusual_sources
77     num, numi = poll_with_sources
78     @polling = false
79     [num, numi]
80   end
81
82   def start
83     @thread = Redwood::reporting_thread("periodic poll") do
84       while true
85         sleep @delay / 2
86         poll if @last_poll.nil? || (Time.now - @last_poll) >= @delay
87       end
88     end
89   end
90
91   def stop
92     @thread.kill if @thread
93     @thread = nil
94   end
95
96   def do_poll
97     total_num = total_numi = 0
98     from_and_subj = []
99     from_and_subj_inbox = []
100     loaded_labels = Set.new
101
102     @mutex.synchronize do
103       @poll_sources.each do |source|
104         begin
105           yield "Loading from #{source}... "
106         rescue SourceError => e
107           warn "problem getting messages from #{source}: #{e.message}"
108           next
109         end
110
111         num = 0
112         numi = 0
113         poll_from source do |action,m,old_m,progress|
114           if action == :delete
115             yield "Deleting #{m.id}"
116           elsif action == :add
117             if old_m
118               if not old_m.locations.member? m.location
119                 yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
120               else
121                 yield "Skipping already-imported message at #{m.source_info}"
122               end
123             else
124               yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
125               loaded_labels.merge m.labels
126               num += 1
127               from_and_subj << [m.from && m.from.longname, m.subj]
128               if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
129                 from_and_subj_inbox << [m.from && m.from.longname, m.subj]
130                 numi += 1
131               end
132             end
133           else fail
134           end
135         end
136         yield "Found #{num} messages, #{numi} to inbox." unless num == 0
137         total_num += num
138         total_numi += numi
139       end
140
141       loaded_labels = loaded_labels - LabelManager::HIDDEN_RESERVED_LABELS - [:inbox, :killed]
142       yield "Done polling; loaded #{total_num} new messages total"
143       @last_poll = Time.now
144       @polling = false
145     end
146     [total_num, total_numi, from_and_subj, from_and_subj_inbox, loaded_labels]
147   end
148
149   ## like Source#poll, but yields successive Message objects, which have their
150   ## labels and locations set correctly. The Messages are saved to or removed
151   ## from the index after being yielded.
152   def poll_from source, opts={}
153     begin
154       source.poll do |sym, args|
155         case sym
156         when :add
157           m = Message.build_from_source source, args[:info]
158           old_m = Index.build_message m.id
159           m.labels += args[:labels]
160           m.labels.delete :inbox  if source.archived?
161           m.labels.delete :unread if source.read?
162           m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
163           m.labels.each { |l| LabelManager << l }
164           m.labels = old_m.labels + (m.labels - [:unread, :inbox]) if old_m
165           m.locations = old_m.locations + m.locations if old_m
166           HookManager.run "before-add-message", :message => m
167           yield :add, m, old_m, args[:progress] if block_given?
168           Index.sync_message m, true
169
170           ## We need to add or unhide the message when it either did not exist
171           ## before at all or when it was updated. We do *not* add/unhide when
172           ## the same message was found at a different location
173           if !old_m or not old_m.locations.member? m.location
174             UpdateManager.relay self, :added, m
175           end
176         when :delete
177           Index.each_message :location => [source.id, args[:info]] do |m|
178             m.locations.delete Location.new(source, args[:info])
179             yield :delete, m, [source,args[:info]], args[:progress] if block_given?
180             Index.sync_message m, false
181             #UpdateManager.relay self, :deleted, m
182           end
183         end
184       end
185     rescue SourceError => e
186       warn "problem getting messages from #{source}: #{e.message}"
187     end
188   end
189
190   def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
191   def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
192   def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :loaded_labels => Set.new}; end
193 end
194
195 end