sup 0.19.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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
@@ -0,0 +1,89 @@
1
+ require 'fileutils'
2
+
3
+ module Redwood
4
+
5
+ ## wrap a nice interactive layer on top of anything that has a #lock method
6
+ ## which throws a LockError which responds to #user, #host, #mtim, #pname, and
7
+ ## #pid.
8
+
9
+ module InteractiveLock
10
+ def pluralize number_of, kind; "#{number_of} #{kind}" + (number_of == 1 ? "" : "s") end
11
+
12
+ def time_ago_in_words time
13
+ secs = (Time.now - time).to_i
14
+ mins = secs / 60
15
+ time = if mins == 0
16
+ pluralize secs, "second"
17
+ else
18
+ pluralize mins, "minute"
19
+ end
20
+ end
21
+
22
+ DELAY = 5 # seconds
23
+
24
+ def lock_interactively stream=$stderr
25
+ begin
26
+ Index.lock
27
+ rescue Index::LockError => e
28
+ begin
29
+ Process.kill 0, e.pid.to_i # 0 signal test the existence of PID
30
+ stream.puts <<EOS
31
+ Error: the index is locked by another process! User '#{e.user}' on
32
+ host '#{e.host}' is running #{e.pname} with pid #{e.pid}.
33
+ The process was alive as of at least #{time_ago_in_words e.mtime} ago.
34
+
35
+ EOS
36
+ stream.print "Should I ask that process to kill itself (y/n)? "
37
+ stream.flush
38
+ if $stdin.gets =~ /^\s*y(es)?\s*$/i
39
+ Process.kill "TERM", e.pid.to_i
40
+ sleep DELAY
41
+ stream.puts "Let's try that again."
42
+ begin
43
+ Index.lock
44
+ rescue Index::LockError => e
45
+ stream.puts "I couldn't lock the index. The lockfile might just be stale."
46
+ stream.print "Should I just remove it and continue? (y/n) "
47
+ stream.flush
48
+ if $stdin.gets =~ /^\s*y(es)?\s*$/i
49
+ begin
50
+ FileUtils.rm e.path
51
+ rescue Errno::ENOENT
52
+ stream.puts "The lockfile doesn't exists. We continue."
53
+ end
54
+ stream.puts "Let's try that one more time."
55
+ begin
56
+ Index.lock
57
+ rescue Index::LockError => e
58
+ stream.puts "I couldn't unlock the index."
59
+ return false
60
+ end
61
+ return true
62
+ end
63
+ end
64
+ end
65
+ rescue Errno::ESRCH # no such process
66
+ stream.puts "I couldn't lock the index. The lockfile might just be stale."
67
+ begin
68
+ FileUtils.rm e.path
69
+ rescue Errno::ENOENT
70
+ stream.puts "The lockfile doesn't exists. We continue."
71
+ end
72
+ stream.puts "Let's try that one more time."
73
+ begin
74
+ sleep DELAY
75
+ Index.lock
76
+ rescue Index::LockError => e
77
+ stream.puts "I couldn't unlock the index."
78
+ return false
79
+ end
80
+ return true
81
+ end
82
+ stream.puts "Sorry, couldn't unlock the index."
83
+ return false
84
+ end
85
+ return true
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,140 @@
1
+ require 'sup/util/ncurses'
2
+
3
+ module Redwood
4
+
5
+ class Keymap
6
+
7
+ HookManager.register "keybindings", <<EOS
8
+ Add custom keybindings.
9
+ Methods:
10
+ modes: Hash from mode names to mode classes.
11
+ global_keymap: The top-level keymap.
12
+ EOS
13
+
14
+ def initialize
15
+ @map = {}
16
+ @order = []
17
+ yield self if block_given?
18
+ end
19
+
20
+ def self.keysym_to_keycode k
21
+ case k
22
+ when :down then Ncurses::KEY_DOWN
23
+ when :up then Ncurses::KEY_UP
24
+ when :left then Ncurses::KEY_LEFT
25
+ when :right then Ncurses::KEY_RIGHT
26
+ when :page_down then Ncurses::KEY_NPAGE
27
+ when :page_up then Ncurses::KEY_PPAGE
28
+ when :backspace then Ncurses::KEY_BACKSPACE
29
+ when :home then Ncurses::KEY_HOME
30
+ when :end then Ncurses::KEY_END
31
+ when :ctrl_l then "\f".ord
32
+ when :ctrl_g then "\a".ord
33
+ when :tab then "\t".ord
34
+ when :enter, :return then 10 #Ncurses::KEY_ENTER
35
+ else
36
+ if k.is_a?(String) && k.length == 1
37
+ k.ord
38
+ else
39
+ raise ArgumentError, "unknown key name '#{k}'"
40
+ end
41
+ end
42
+ end
43
+
44
+ def self.keysym_to_string k
45
+ case k
46
+ when :down then "<down arrow>"
47
+ when :up then "<up arrow>"
48
+ when :left then "<left arrow>"
49
+ when :right then "<right arrow>"
50
+ when :page_down then "<page down>"
51
+ when :page_up then "<page up>"
52
+ when :backspace then "<backspace>"
53
+ when :home then "<home>"
54
+ when :end then "<end>"
55
+ when :enter, :return then "<enter>"
56
+ when :tab then "tab"
57
+ when " " then "<space>"
58
+ else
59
+ Ncurses::keyname(keysym_to_keycode(k))
60
+ end
61
+ end
62
+
63
+ def add action, help, *keys
64
+ entry = [action, help, keys]
65
+ @order << entry
66
+ keys.each do |k|
67
+ kc = Keymap.keysym_to_keycode k
68
+ raise ArgumentError, "key '#{k}' already defined (as #{@map[kc].first})" if @map.include? kc
69
+ @map[kc] = entry
70
+ end
71
+ end
72
+
73
+ def delete k
74
+ kc = Keymap.keysym_to_keycode(k)
75
+ return unless @map.member? kc
76
+ entry = @map.delete kc
77
+ keys = entry[2]
78
+ keys.delete k
79
+ @order.delete entry if keys.empty?
80
+ end
81
+
82
+ def add! action, help, *keys
83
+ keys.each { |k| delete k }
84
+ add action, help, *keys
85
+ end
86
+
87
+ def add_multi prompt, key
88
+ kc = Keymap.keysym_to_keycode(key)
89
+ if @map.member? kc
90
+ action = @map[kc].first
91
+ raise "existing action is not a keymap" unless action.is_a?(Keymap)
92
+ yield action
93
+ else
94
+ submap = Keymap.new
95
+ add submap, prompt, key
96
+ yield submap
97
+ end
98
+ end
99
+
100
+ def action_for kc
101
+ action, help, keys = @map[kc.code]
102
+ [action, help]
103
+ end
104
+
105
+ def has_key? k; @map[k.code] end
106
+
107
+ def keysyms; @map.values.map { |action, help, keys| keys }.flatten; end
108
+
109
+ def help_lines except_for={}, prefix=""
110
+ lines = [] # :(
111
+ @order.each do |action, help, keys|
112
+ valid_keys = keys.select { |k| !except_for[k] }
113
+ next if valid_keys.empty?
114
+ case action
115
+ when Symbol
116
+ lines << [valid_keys.map { |k| prefix + Keymap.keysym_to_string(k) }.join(", "), help]
117
+ when Keymap
118
+ lines += action.help_lines({}, prefix + Keymap.keysym_to_string(keys.first))
119
+ end
120
+ end.compact
121
+ lines
122
+ end
123
+
124
+ def help_text except_for={}
125
+ lines = help_lines except_for
126
+ llen = lines.max_of { |a, b| a.length }
127
+ lines.map { |a, b| sprintf " %#{llen}s : %s", a, b }.join("\n")
128
+ end
129
+
130
+ def self.run_hook global_keymap
131
+ modes = Hash[Mode.keymaps.map { |klass,keymap| [Mode.make_name(klass.name),klass] }]
132
+ locals = {
133
+ :modes => modes,
134
+ :global_keymap => global_keymap,
135
+ }
136
+ HookManager.run 'keybindings', locals
137
+ end
138
+ end
139
+
140
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ module Redwood
4
+
5
+ class LabelManager
6
+ include Redwood::Singleton
7
+
8
+ ## labels that have special semantics. user will be unable to
9
+ ## add/remove these via normal label mechanisms.
10
+ RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent, :deleted, :inbox, :attachment, :forwarded, :replied ]
11
+
12
+ ## labels that will typically be hidden from the user
13
+ HIDDEN_RESERVED_LABELS = [ :starred, :unread, :attachment, :forwarded, :replied ]
14
+
15
+ def initialize fn
16
+ @fn = fn
17
+ labels =
18
+ if File.exists? fn
19
+ IO.readlines(fn).map { |x| x.chomp.intern }
20
+ else
21
+ []
22
+ end
23
+ @labels = {}
24
+ @new_labels = {}
25
+ @modified = false
26
+ labels.each { |t| @labels[t] = true }
27
+ end
28
+
29
+ def new_label? l; @new_labels.include?(l) end
30
+
31
+ ## all labels user-defined and system, ordered
32
+ ## nicely and converted to pretty strings. use #label_for to recover
33
+ ## the original label.
34
+ def all_labels
35
+ ## uniq's only necessary here because of certain upgrade issues
36
+ (RESERVED_LABELS + @labels.keys).uniq
37
+ end
38
+
39
+ ## all user-defined labels, ordered
40
+ ## nicely and converted to pretty strings. use #label_for to recover
41
+ ## the original label.
42
+ def user_defined_labels
43
+ @labels.keys
44
+ end
45
+
46
+ ## reverse the label->string mapping, for convenience!
47
+ def string_for l
48
+ if RESERVED_LABELS.include? l
49
+ l.to_s.capitalize
50
+ else
51
+ l.to_s
52
+ end
53
+ end
54
+
55
+ def label_for s
56
+ l = s.intern
57
+ l2 = s.downcase.intern
58
+ if RESERVED_LABELS.include? l2
59
+ l2
60
+ else
61
+ l
62
+ end
63
+ end
64
+
65
+ def << t
66
+ raise ArgumentError, "expecting a symbol" unless t.is_a? Symbol
67
+ unless @labels.member?(t) || RESERVED_LABELS.member?(t)
68
+ @labels[t] = true
69
+ @new_labels[t] = true
70
+ @modified = true
71
+ end
72
+ end
73
+
74
+ def delete t
75
+ if @labels.delete(t)
76
+ @modified = true
77
+ end
78
+ end
79
+
80
+ def save
81
+ return unless @modified
82
+ File.open(@fn, "w:UTF-8") { |f| f.puts @labels.keys.sort_by { |l| l.to_s } }
83
+ @new_labels = {}
84
+ end
85
+ end
86
+
87
+ end
@@ -0,0 +1,77 @@
1
+ require "sup/util"
2
+ require 'stringio'
3
+ require 'thread'
4
+
5
+ module Redwood
6
+
7
+ ## simple centralized logger. outputs to multiple sinks by calling << on them.
8
+ ## also keeps a record of all messages, so that adding a new sink will send all
9
+ ## previous messages to it by default.
10
+ class Logger
11
+ include Redwood::Singleton
12
+
13
+ LEVELS = %w(debug info warn error) # in order!
14
+
15
+ def initialize level=nil
16
+ level ||= ENV["SUP_LOG_LEVEL"] || "info"
17
+ self.level = level
18
+ @mutex = Mutex.new
19
+ @buf = StringIO.new
20
+ @sinks = []
21
+ end
22
+
23
+ def level; LEVELS[@level] end
24
+ def level=(level); @level = LEVELS.index(level) || raise(ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"); end
25
+
26
+ def add_sink s, copy_current=true
27
+ @mutex.synchronize do
28
+ @sinks << s
29
+ s << @buf.string if copy_current
30
+ end
31
+ end
32
+
33
+ def remove_sink s; @mutex.synchronize { @sinks.delete s } end
34
+ def remove_all_sinks!; @mutex.synchronize { @sinks.clear } end
35
+ def clear!; @mutex.synchronize { @buf = StringIO.new } end
36
+
37
+ LEVELS.each_with_index do |l, method_level|
38
+ define_method(l) do |s|
39
+ if method_level >= @level
40
+ send_message format_message(l, Time.now, s)
41
+ end
42
+ end
43
+ end
44
+
45
+ ## send a message regardless of the current logging level
46
+ def force_message m; send_message format_message(nil, Time.now, m) end
47
+
48
+ private
49
+
50
+ ## level can be nil!
51
+ def format_message level, time, msg
52
+ prefix = case level
53
+ when "warn"; "WARNING: "
54
+ when "error"; "ERROR: "
55
+ else ""
56
+ end
57
+ "[#{time.to_s}] #{prefix}#{msg.rstrip}\n"
58
+ end
59
+
60
+ ## actually distribute the message
61
+ def send_message m
62
+ @mutex.synchronize do
63
+ @sinks.each do |sink|
64
+ sink << m
65
+ sink.flush if sink.respond_to?(:flush) and level == "debug"
66
+ end
67
+ @buf << m
68
+ end
69
+ end
70
+ end
71
+
72
+ ## include me to have top-level #debug, #info, etc. methods.
73
+ module LogsStuff
74
+ Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
75
+ end
76
+
77
+ end
@@ -0,0 +1,10 @@
1
+ # TODO: this is ugly. It's better to have a application singleton passed
2
+ # down to lower level components instead of including logging methods in
3
+ # class `Object'
4
+ #
5
+ # For now this is what we have to do.
6
+ require "sup/logger"
7
+ Redwood::Logger.init.add_sink $stderr
8
+ class Object
9
+ include Redwood::LogsStuff
10
+ end
@@ -0,0 +1,257 @@
1
+ require 'uri'
2
+ require 'set'
3
+
4
+ module Redwood
5
+
6
+ class Maildir < Source
7
+ include SerializeLabelsNicely
8
+ MYHOSTNAME = Socket.gethostname
9
+
10
+ ## remind me never to use inheritance again.
11
+ yaml_properties :uri, :usual, :archived, :sync_back, :id, :labels
12
+ def initialize uri, usual=true, archived=false, sync_back=true, id=nil, labels=[]
13
+ super uri, usual, archived, id
14
+ @expanded_uri = Source.expand_filesystem_uri(uri)
15
+ uri = URI(@expanded_uri)
16
+
17
+ raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
18
+ raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
19
+ raise ArgumentError, "maildir URI must have a path component" unless uri.path
20
+
21
+ @sync_back = sync_back
22
+ # sync by default if not specified
23
+ @sync_back = true if @sync_back.nil?
24
+
25
+ @dir = uri.path
26
+ @labels = Set.new(labels || [])
27
+ @mutex = Mutex.new
28
+ @ctimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
29
+ end
30
+
31
+ def file_path; @dir end
32
+ def self.suggest_labels_for path; [] end
33
+ def is_source_for? uri; super || (uri == @expanded_uri); end
34
+
35
+ def supported_labels?
36
+ [:draft, :starred, :forwarded, :replied, :unread, :deleted]
37
+ end
38
+
39
+ def sync_back_enabled?
40
+ @sync_back
41
+ end
42
+
43
+ def store_message date, from_email, &block
44
+ stored = false
45
+ new_fn = new_maildir_basefn + ':2,S'
46
+ Dir.chdir(@dir) do |d|
47
+ tmp_path = File.join(@dir, 'tmp', new_fn)
48
+ new_path = File.join(@dir, 'new', new_fn)
49
+ begin
50
+ sleep 2 if File.stat(tmp_path)
51
+
52
+ File.stat(tmp_path)
53
+ rescue Errno::ENOENT #this is what we want.
54
+ begin
55
+ File.open(tmp_path, 'wb') do |f|
56
+ yield f #provide a writable interface for the caller
57
+ f.fsync
58
+ end
59
+
60
+ File.safe_link tmp_path, new_path
61
+ stored = true
62
+ ensure
63
+ File.unlink tmp_path if File.exists? tmp_path
64
+ end
65
+ end #rescue Errno...
66
+ end #Dir.chdir
67
+
68
+ stored
69
+ end
70
+
71
+ def each_raw_message_line id
72
+ with_file_for(id) do |f|
73
+ until f.eof?
74
+ yield f.gets
75
+ end
76
+ end
77
+ end
78
+
79
+ def load_header id
80
+ with_file_for(id) { |f| parse_raw_email_header f }
81
+ end
82
+
83
+ def load_message id
84
+ with_file_for(id) { |f| RMail::Parser.read f }
85
+ end
86
+
87
+ def sync_back id, labels
88
+ synchronize do
89
+ debug "syncing back maildir message #{id} with flags #{labels.to_a}"
90
+ flags = maildir_reconcile_flags id, labels
91
+ maildir_mark_file id, flags
92
+ end
93
+ end
94
+
95
+ def raw_header id
96
+ ret = ""
97
+ with_file_for(id) do |f|
98
+ until f.eof? || (l = f.gets) =~ /^$/
99
+ ret += l
100
+ end
101
+ end
102
+ ret
103
+ end
104
+
105
+ def raw_message id
106
+ with_file_for(id) { |f| f.read }
107
+ end
108
+
109
+ ## XXX use less memory
110
+ def poll
111
+ added = []
112
+ deleted = []
113
+ updated = []
114
+ @ctimes.each do |d,prev_ctime|
115
+ subdir = File.join @dir, d
116
+ debug "polling maildir #{subdir}"
117
+ raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
118
+ ctime = File.ctime subdir
119
+ next if prev_ctime >= ctime
120
+ @ctimes[d] = ctime
121
+
122
+ old_ids = benchmark(:maildir_read_index) { Index.instance.enum_for(:each_source_info, self.id, "#{d}/").to_a }
123
+ new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.join(d,File.basename(x)) }.sort }
124
+ added += new_ids - old_ids
125
+ deleted += old_ids - new_ids
126
+ debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
127
+ end
128
+
129
+ ## find updated mails by checking if an id is in both added and
130
+ ## deleted arrays, meaning that its flags changed or that it has
131
+ ## been moved, these ids need to be removed from added and deleted
132
+ add_to_delete = del_to_delete = []
133
+ map = Hash.new { |hash, key| hash[key] = [] }
134
+ deleted.each do |id_del|
135
+ map[maildir_data(id_del)[0]].push id_del
136
+ end
137
+ added.each do |id_add|
138
+ map[maildir_data(id_add)[0]].each do |id_del|
139
+ updated.push [ id_del, id_add ]
140
+ add_to_delete.push id_add
141
+ del_to_delete.push id_del
142
+ end
143
+ end
144
+ added -= add_to_delete
145
+ deleted -= del_to_delete
146
+ debug "#{added.size} added, #{deleted.size} deleted, #{updated.size} updated"
147
+ total_size = added.size+deleted.size+updated.size
148
+
149
+ added.each_with_index do |id,i|
150
+ yield :add,
151
+ :info => id,
152
+ :labels => @labels + maildir_labels(id) + [:inbox],
153
+ :progress => i.to_f/total_size
154
+ end
155
+
156
+ deleted.each_with_index do |id,i|
157
+ yield :delete,
158
+ :info => id,
159
+ :progress => (i.to_f+added.size)/total_size
160
+ end
161
+
162
+ updated.each_with_index do |id,i|
163
+ yield :update,
164
+ :old_info => id[0],
165
+ :new_info => id[1],
166
+ :labels => @labels + maildir_labels(id[1]),
167
+ :progress => (i.to_f+added.size+deleted.size)/total_size
168
+ end
169
+ nil
170
+ end
171
+
172
+ def labels? id
173
+ maildir_labels id
174
+ end
175
+
176
+ def maildir_labels id
177
+ (seen?(id) ? [] : [:unread]) +
178
+ (trashed?(id) ? [:deleted] : []) +
179
+ (flagged?(id) ? [:starred] : []) +
180
+ (passed?(id) ? [:forwarded] : []) +
181
+ (replied?(id) ? [:replied] : []) +
182
+ (draft?(id) ? [:draft] : [])
183
+ end
184
+
185
+ def draft? id; maildir_data(id)[2].include? "D"; end
186
+ def flagged? id; maildir_data(id)[2].include? "F"; end
187
+ def passed? id; maildir_data(id)[2].include? "P"; end
188
+ def replied? id; maildir_data(id)[2].include? "R"; end
189
+ def seen? id; maildir_data(id)[2].include? "S"; end
190
+ def trashed? id; maildir_data(id)[2].include? "T"; end
191
+
192
+ def valid? id
193
+ File.exists? File.join(@dir, id)
194
+ end
195
+
196
+ private
197
+
198
+ def new_maildir_basefn
199
+ Kernel::srand()
200
+ "#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
201
+ end
202
+
203
+ def with_file_for id
204
+ fn = File.join(@dir, id)
205
+ begin
206
+ File.open(fn, 'rb') { |f| yield f }
207
+ rescue SystemCallError, IOError => e
208
+ raise FatalSourceError, "Problem reading file for id #{id.inspect}: #{fn.inspect}: #{e.message}."
209
+ end
210
+ end
211
+
212
+ def maildir_data id
213
+ id = File.basename id
214
+ # Flags we recognize are DFPRST
215
+ id =~ %r{^([^:]+):([12]),([A-Za-z]*)$}
216
+ [($1 || id), ($2 || "2"), ($3 || "")]
217
+ end
218
+
219
+ def maildir_reconcile_flags id, labels
220
+ new_flags = Set.new( maildir_data(id)[2].each_char )
221
+
222
+ # Set flags based on labels for the six flags we recognize
223
+ if labels.member? :draft then new_flags.add?( "D" ) else new_flags.delete?( "D" ) end
224
+ if labels.member? :starred then new_flags.add?( "F" ) else new_flags.delete?( "F" ) end
225
+ if labels.member? :forwarded then new_flags.add?( "P" ) else new_flags.delete?( "P" ) end
226
+ if labels.member? :replied then new_flags.add?( "R" ) else new_flags.delete?( "R" ) end
227
+ if not labels.member? :unread then new_flags.add?( "S" ) else new_flags.delete?( "S" ) end
228
+ if labels.member? :deleted or labels.member? :killed then new_flags.add?( "T" ) else new_flags.delete?( "T" ) end
229
+
230
+ ## Flags must be stored in ASCII order according to Maildir
231
+ ## documentation
232
+ new_flags.to_a.sort.join
233
+ end
234
+
235
+ def maildir_mark_file orig_path, flags
236
+ @mutex.synchronize do
237
+ new_base = (flags.include?("S")) ? "cur" : "new"
238
+ md_base, md_ver, md_flags = maildir_data orig_path
239
+
240
+ return if md_flags == flags
241
+
242
+ new_loc = File.join new_base, "#{md_base}:#{md_ver},#{flags}"
243
+ orig_path = File.join @dir, orig_path
244
+ new_path = File.join @dir, new_loc
245
+ tmp_path = File.join @dir, "tmp", "#{md_base}:#{md_ver},#{flags}"
246
+
247
+ File.safe_link orig_path, tmp_path
248
+ File.unlink orig_path
249
+ File.safe_link tmp_path, new_path
250
+ File.unlink tmp_path
251
+
252
+ new_loc
253
+ end
254
+ end
255
+ end
256
+
257
+ end