sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
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