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.
- 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
|