sup 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
data/lib/sup/search.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
|
5
|
+
class SearchManager
|
6
|
+
include Redwood::Singleton
|
7
|
+
|
8
|
+
class ExpansionError < StandardError; end
|
9
|
+
|
10
|
+
attr_reader :predefined_searches
|
11
|
+
|
12
|
+
def initialize fn
|
13
|
+
@fn = fn
|
14
|
+
@searches = {}
|
15
|
+
if File.exists? fn
|
16
|
+
IO.foreach(fn) do |l|
|
17
|
+
l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
|
18
|
+
@searches[$1] = $2
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@modified = false
|
22
|
+
|
23
|
+
@predefined_searches = { 'All mail' => 'Search all mail.' }
|
24
|
+
@predefined_queries = { 'All mail'.to_sym => { :qobj => Xapian::Query.new('Kmail'),
|
25
|
+
:load_spam => false,
|
26
|
+
:load_deleted => false,
|
27
|
+
:load_killed => false,
|
28
|
+
:text => 'Search all mail.'}
|
29
|
+
}
|
30
|
+
@predefined_searches.each do |k,v|
|
31
|
+
@searches[k] = v
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def predefined_queries; return @predefined_queries; end
|
36
|
+
def all_searches; return @searches.keys.sort; end
|
37
|
+
def search_string_for name;
|
38
|
+
if @predefined_searches.keys.member? name
|
39
|
+
return name.to_sym
|
40
|
+
end
|
41
|
+
return @searches[name];
|
42
|
+
end
|
43
|
+
def valid_name? name; name =~ /^[\w-]+$/; end
|
44
|
+
def name_format_hint; "letters, numbers, underscores and dashes only"; end
|
45
|
+
|
46
|
+
def add name, search_string
|
47
|
+
return unless valid_name? name
|
48
|
+
if @predefined_searches.has_key? name
|
49
|
+
warn "cannot add search: #{name} is already taken by a predefined search"
|
50
|
+
return
|
51
|
+
end
|
52
|
+
@searches[name] = search_string
|
53
|
+
@modified = true
|
54
|
+
end
|
55
|
+
|
56
|
+
def rename old, new
|
57
|
+
return unless @searches.has_key? old
|
58
|
+
if [old, new].any? { |x| @predefined_searches.has_key? x }
|
59
|
+
warn "cannot rename search: #{old} or #{new} is already taken by a predefined search"
|
60
|
+
return
|
61
|
+
end
|
62
|
+
search_string = @searches[old]
|
63
|
+
delete old if add new, search_string
|
64
|
+
end
|
65
|
+
|
66
|
+
def edit name, search_string
|
67
|
+
return unless @searches.has_key? name
|
68
|
+
if @predefined_searches.has_key? name
|
69
|
+
warn "cannot edit predefined search: #{name}."
|
70
|
+
return
|
71
|
+
end
|
72
|
+
@searches[name] = search_string
|
73
|
+
@modified = true
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete name
|
77
|
+
return unless @searches.has_key? name
|
78
|
+
if @predefined_searches.has_key? name
|
79
|
+
warn "cannot delete predefined search: #{name}."
|
80
|
+
return
|
81
|
+
end
|
82
|
+
@searches.delete name
|
83
|
+
@modified = true
|
84
|
+
end
|
85
|
+
|
86
|
+
def expand search_string
|
87
|
+
expanded = search_string.dup
|
88
|
+
until (matches = expanded.scan(/\{([\w-]+)\}/).flatten).empty?
|
89
|
+
if !(unknown = matches - @searches.keys).empty?
|
90
|
+
error_message = "Unknown \"#{unknown.join('", "')}\" when expanding \"#{search_string}\""
|
91
|
+
elsif expanded.size >= 2048
|
92
|
+
error_message = "Check for infinite recursion in \"#{search_string}\""
|
93
|
+
end
|
94
|
+
if error_message
|
95
|
+
warn error_message
|
96
|
+
raise ExpansionError, error_message
|
97
|
+
end
|
98
|
+
matches.each { |n| expanded.gsub! "{#{n}}", "(#{@searches[n]})" if @searches.has_key? n }
|
99
|
+
end
|
100
|
+
return expanded
|
101
|
+
end
|
102
|
+
|
103
|
+
def save
|
104
|
+
return unless @modified
|
105
|
+
File.open(@fn, "w:UTF-8") { |f| (@searches - @predefined_searches.keys).sort.each { |(n, s)| f.puts "#{n}: #{s}" } }
|
106
|
+
@modified = false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
data/lib/sup/sent.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Redwood
|
2
|
+
|
3
|
+
class SentManager
|
4
|
+
include Redwood::Singleton
|
5
|
+
|
6
|
+
attr_reader :source, :source_uri
|
7
|
+
|
8
|
+
def initialize source_uri
|
9
|
+
@source = nil
|
10
|
+
@source_uri = source_uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def source_id; @source.id; end
|
14
|
+
|
15
|
+
def source= s
|
16
|
+
raise FatalSourceError.new("Configured sent_source [#{s.uri}] can't store mail. Correct your configuration.") unless s.respond_to? :store_message
|
17
|
+
@source_uri = s.uri
|
18
|
+
@source = s
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_source
|
22
|
+
@source = SentLoader.new
|
23
|
+
@source_uri = @source.uri
|
24
|
+
@source
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_sent_message date, from_email, &block
|
28
|
+
::Thread.new do
|
29
|
+
debug "store the sent message (locking sent source..)"
|
30
|
+
@source.synchronize do
|
31
|
+
@source.store_message date, from_email, &block
|
32
|
+
end
|
33
|
+
PollManager.poll_from @source
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SentLoader < MBox
|
39
|
+
yaml_properties
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@filename = Redwood::SENT_FN
|
43
|
+
File.open(@filename, "w") { } unless File.exists? @filename
|
44
|
+
super "mbox://" + @filename, true, $config[:archive_sent]
|
45
|
+
end
|
46
|
+
|
47
|
+
def file_path; @filename end
|
48
|
+
|
49
|
+
def to_s; 'sup://sent'; end
|
50
|
+
def uri; 'sup://sent' end
|
51
|
+
|
52
|
+
def id; 9998; end
|
53
|
+
def labels; [:inbox, :sent]; end
|
54
|
+
def default_labels; []; end
|
55
|
+
def read?; true; end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "sup/index"
|
2
|
+
|
3
|
+
module Redwood
|
4
|
+
# Provides label tweaking service to the user.
|
5
|
+
# Working as the backend of ConsoleMode.
|
6
|
+
#
|
7
|
+
# Should become the backend of bin/sup-tweak-labels in the future.
|
8
|
+
class LabelService
|
9
|
+
# @param index [Redwood::Index]
|
10
|
+
def initialize index=Index.instance
|
11
|
+
@index = index
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_labels query, *labels
|
15
|
+
run_on_each_message(query) do |m|
|
16
|
+
labels.each {|l| m.add_label l }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_labels query, *labels
|
21
|
+
run_on_each_message(query) do |m|
|
22
|
+
labels.each {|l| m.remove_label l }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
private
|
28
|
+
def run_on_each_message query, &operation
|
29
|
+
count = 0
|
30
|
+
|
31
|
+
find_messages(query).each do |m|
|
32
|
+
operation.call(m)
|
33
|
+
@index.update_message_state m
|
34
|
+
count += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
@index.save_index
|
38
|
+
count
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_messages query
|
42
|
+
@index.find_messages(query)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/sup/source.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
require "sup/rfc2047"
|
2
|
+
require "monitor"
|
3
|
+
|
4
|
+
module Redwood
|
5
|
+
|
6
|
+
class SourceError < StandardError
|
7
|
+
def initialize *a
|
8
|
+
raise "don't instantiate me!" if SourceError.is_a?(self.class)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class OutOfSyncSourceError < SourceError; end
|
13
|
+
class FatalSourceError < SourceError; end
|
14
|
+
|
15
|
+
class Source
|
16
|
+
## Implementing a new source should be easy, because Sup only needs
|
17
|
+
## to be able to:
|
18
|
+
## 1. See how many messages it contains
|
19
|
+
## 2. Get an arbitrary message
|
20
|
+
## 3. (optional) see whether the source has marked it read or not
|
21
|
+
##
|
22
|
+
## In particular, Sup doesn't need to move messages, mark them as
|
23
|
+
## read, delete them, or anything else. (Well, it's nice to be able
|
24
|
+
## to delete them, but that is optional.)
|
25
|
+
##
|
26
|
+
## Messages are identified internally based on the message id, and stored
|
27
|
+
## with an unique document id. Along with the message, source information
|
28
|
+
## that can contain arbitrary fields (set up by the source) is stored. This
|
29
|
+
## information will be passed back to the source when a message in the
|
30
|
+
## index (Sup database) needs to be identified to its source, e.g. when
|
31
|
+
## re-reading or modifying a unique message.
|
32
|
+
##
|
33
|
+
## To write a new source, subclass this class, and implement:
|
34
|
+
##
|
35
|
+
## - initialize
|
36
|
+
## - load_header offset
|
37
|
+
## - load_message offset
|
38
|
+
## - raw_header offset
|
39
|
+
## - raw_message offset
|
40
|
+
## - store_message (optional)
|
41
|
+
## - poll (loads new messages)
|
42
|
+
## - go_idle (optional)
|
43
|
+
##
|
44
|
+
## All exceptions relating to accessing the source must be caught
|
45
|
+
## and rethrown as FatalSourceErrors or OutOfSyncSourceErrors.
|
46
|
+
## OutOfSyncSourceErrors should be used for problems that a call to
|
47
|
+
## sup-sync will fix (namely someone's been playing with the source
|
48
|
+
## from another client); FatalSourceErrors can be used for anything
|
49
|
+
## else (e.g. the imap server is down or the maildir is missing.)
|
50
|
+
##
|
51
|
+
## Finally, be sure the source is thread-safe, since it WILL be
|
52
|
+
## pummelled from multiple threads at once.
|
53
|
+
##
|
54
|
+
## Examples for you to look at: mbox.rb and maildir.rb.
|
55
|
+
|
56
|
+
bool_accessor :usual, :archived
|
57
|
+
attr_reader :uri, :usual
|
58
|
+
attr_accessor :id
|
59
|
+
|
60
|
+
def initialize uri, usual=true, archived=false, id=nil
|
61
|
+
raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
|
62
|
+
|
63
|
+
@uri = uri
|
64
|
+
@usual = usual
|
65
|
+
@archived = archived
|
66
|
+
@id = id
|
67
|
+
|
68
|
+
@poll_lock = Monitor.new
|
69
|
+
end
|
70
|
+
|
71
|
+
## overwrite me if you have a disk incarnation
|
72
|
+
def file_path; nil end
|
73
|
+
|
74
|
+
def to_s; @uri.to_s; end
|
75
|
+
def == o; o.uri == uri; end
|
76
|
+
def is_source_for? uri; uri == @uri; end
|
77
|
+
|
78
|
+
def read?; false; end
|
79
|
+
|
80
|
+
## release resources that are easy to reacquire. it is called
|
81
|
+
## after processing a source (e.g. polling) to prevent resource
|
82
|
+
## leaks (esp. file descriptors).
|
83
|
+
def go_idle; end
|
84
|
+
|
85
|
+
## Returns an array containing all the labels that are natively
|
86
|
+
## supported by this source
|
87
|
+
def supported_labels?; [] end
|
88
|
+
|
89
|
+
## Returns an array containing all the labels that are currently in
|
90
|
+
## the location filename
|
91
|
+
def labels? info; [] end
|
92
|
+
|
93
|
+
## Yields values of the form [Symbol, Hash]
|
94
|
+
## add: info, labels, progress
|
95
|
+
## delete: info, progress
|
96
|
+
def poll
|
97
|
+
unimplemented
|
98
|
+
end
|
99
|
+
|
100
|
+
def valid? info
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def synchronize &block
|
105
|
+
@poll_lock.synchronize &block
|
106
|
+
end
|
107
|
+
|
108
|
+
def try_lock
|
109
|
+
acquired = @poll_lock.try_enter
|
110
|
+
if acquired
|
111
|
+
debug "lock acquired for: #{self}"
|
112
|
+
else
|
113
|
+
debug "could not acquire lock for: #{self}"
|
114
|
+
end
|
115
|
+
acquired
|
116
|
+
end
|
117
|
+
|
118
|
+
def unlock
|
119
|
+
@poll_lock.exit
|
120
|
+
debug "lock released for: #{self}"
|
121
|
+
end
|
122
|
+
|
123
|
+
## utility method to read a raw email header from an IO stream and turn it
|
124
|
+
## into a hash of key-value pairs. minor special semantics for certain headers.
|
125
|
+
##
|
126
|
+
## THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have a
|
127
|
+
## significant effect on Sup's processing speed of email from ALL sources.
|
128
|
+
## Little things like string interpolation, regexp interpolation, += vs <<,
|
129
|
+
## all have DRAMATIC effects. BE CAREFUL WHAT YOU DO!
|
130
|
+
def self.parse_raw_email_header f
|
131
|
+
header = {}
|
132
|
+
last = nil
|
133
|
+
|
134
|
+
while(line = f.gets)
|
135
|
+
case line
|
136
|
+
## these three can occur multiple times, and we want the first one
|
137
|
+
when /^(Delivered-To|X-Original-To|Envelope-To):\s*(.*?)\s*$/i; header[last = $1.downcase] ||= $2
|
138
|
+
## regular header: overwrite (not that we should see more than one)
|
139
|
+
## TODO: figure out whether just using the first occurrence changes
|
140
|
+
## anything (which would simplify the logic slightly)
|
141
|
+
when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
|
142
|
+
when /^\r*$/; break # blank line signifies end of header
|
143
|
+
else
|
144
|
+
if last
|
145
|
+
header[last] << " " unless header[last].empty?
|
146
|
+
header[last] << line.strip
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
%w(subject from to cc bcc).each do |k|
|
152
|
+
v = header[k] or next
|
153
|
+
next unless Rfc2047.is_encoded? v
|
154
|
+
header[k] = begin
|
155
|
+
Rfc2047.decode_to $encoding, v
|
156
|
+
rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
|
157
|
+
#debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
|
158
|
+
v
|
159
|
+
end
|
160
|
+
end
|
161
|
+
header
|
162
|
+
end
|
163
|
+
|
164
|
+
protected
|
165
|
+
|
166
|
+
## convenience function
|
167
|
+
def parse_raw_email_header f; self.class.parse_raw_email_header f end
|
168
|
+
|
169
|
+
def Source.expand_filesystem_uri uri
|
170
|
+
uri.gsub "~", File.expand_path("~")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
## if you have a @labels instance variable, include this
|
175
|
+
## to serialize them nicely as an array, rather than as a
|
176
|
+
## nasty set.
|
177
|
+
module SerializeLabelsNicely
|
178
|
+
def before_marshal # can return an object
|
179
|
+
c = clone
|
180
|
+
c.instance_eval { @labels = (@labels.to_a.map { |l| l.to_s }).sort }
|
181
|
+
c
|
182
|
+
end
|
183
|
+
|
184
|
+
def after_unmarshal!
|
185
|
+
@labels = Set.new(@labels.to_a.map { |s| s.to_sym })
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
class SourceManager
|
190
|
+
include Redwood::Singleton
|
191
|
+
|
192
|
+
def initialize
|
193
|
+
@sources = {}
|
194
|
+
@sources_dirty = false
|
195
|
+
@source_mutex = Monitor.new
|
196
|
+
end
|
197
|
+
|
198
|
+
def [](id)
|
199
|
+
@source_mutex.synchronize { @sources[id] }
|
200
|
+
end
|
201
|
+
|
202
|
+
def add_source source
|
203
|
+
@source_mutex.synchronize do
|
204
|
+
raise "duplicate source!" if @sources.include? source
|
205
|
+
@sources_dirty = true
|
206
|
+
max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
|
207
|
+
source.id ||= (max || 0) + 1
|
208
|
+
##source.id += 1 while @sources.member? source.id
|
209
|
+
@sources[source.id] = source
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def sources
|
214
|
+
## favour the inbox by listing non-archived sources first
|
215
|
+
@source_mutex.synchronize { @sources.values }.sort_by { |s| s.id }.partition { |s| !s.archived? }.flatten
|
216
|
+
end
|
217
|
+
|
218
|
+
def source_for uri
|
219
|
+
expanded_uri = Source.expand_filesystem_uri(uri)
|
220
|
+
sources.find { |s| s.is_source_for? expanded_uri }
|
221
|
+
end
|
222
|
+
|
223
|
+
def usual_sources; sources.find_all { |s| s.usual? }; end
|
224
|
+
def unusual_sources; sources.find_all { |s| !s.usual? }; end
|
225
|
+
|
226
|
+
def load_sources fn=Redwood::SOURCE_FN
|
227
|
+
source_array = Redwood::load_yaml_obj(fn) || []
|
228
|
+
@source_mutex.synchronize do
|
229
|
+
@sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
|
230
|
+
@sources_dirty = false
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def save_sources fn=Redwood::SOURCE_FN, force=false
|
235
|
+
@source_mutex.synchronize do
|
236
|
+
if @sources_dirty || force
|
237
|
+
Redwood::save_yaml_obj sources, fn, false, true
|
238
|
+
end
|
239
|
+
@sources_dirty = false
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|