sup 0.11 → 0.12
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/CONTRIBUTORS +16 -5
- data/History.txt +9 -0
- data/ReleaseNotes +9 -0
- data/bin/sup +9 -24
- data/bin/sup-add +3 -3
- data/bin/sup-config +1 -1
- data/bin/sup-dump +22 -9
- data/bin/sup-recover-sources +1 -11
- data/bin/sup-sync +65 -130
- data/bin/sup-sync-back +6 -5
- data/bin/sup-tweak-labels +5 -6
- data/lib/sup.rb +89 -71
- data/lib/sup/account.rb +3 -2
- data/lib/sup/buffer.rb +28 -16
- data/lib/sup/client.rb +92 -0
- data/lib/sup/crypto.rb +91 -49
- data/lib/sup/draft.rb +14 -17
- data/lib/sup/hook.rb +10 -5
- data/lib/sup/index.rb +72 -28
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +55 -112
- data/lib/sup/mbox.rb +151 -6
- data/lib/sup/message-chunks.rb +20 -4
- data/lib/sup/message.rb +183 -76
- data/lib/sup/modes/compose-mode.rb +2 -1
- data/lib/sup/modes/console-mode.rb +4 -1
- data/lib/sup/modes/edit-message-mode.rb +50 -5
- data/lib/sup/modes/line-cursor-mode.rb +1 -0
- data/lib/sup/modes/reply-mode.rb +17 -11
- data/lib/sup/modes/thread-index-mode.rb +10 -9
- data/lib/sup/modes/thread-view-mode.rb +48 -2
- data/lib/sup/poll.rb +56 -60
- data/lib/sup/protocol.rb +161 -0
- data/lib/sup/sent.rb +8 -11
- data/lib/sup/server.rb +116 -0
- data/lib/sup/source.rb +15 -33
- data/lib/sup/thread.rb +6 -0
- data/lib/sup/util.rb +44 -39
- metadata +126 -88
- data/lib/sup/connection.rb +0 -63
- data/lib/sup/imap.rb +0 -349
- data/lib/sup/mbox/loader.rb +0 -180
- data/lib/sup/mbox/ssh-file.rb +0 -254
- data/lib/sup/mbox/ssh-loader.rb +0 -74
data/lib/sup/logger.rb
CHANGED
data/lib/sup/maildir.rb
CHANGED
@@ -1,22 +1,15 @@
|
|
1
|
-
require 'rmail'
|
2
1
|
require 'uri'
|
3
2
|
|
4
3
|
module Redwood
|
5
4
|
|
6
|
-
## Maildir doesn't provide an ordered unique id, which is what Sup
|
7
|
-
## requires to be really useful. So we must maintain, in memory, a
|
8
|
-
## mapping between Sup "ids" (timestamps, essentially) and the
|
9
|
-
## pathnames on disk.
|
10
|
-
|
11
5
|
class Maildir < Source
|
12
6
|
include SerializeLabelsNicely
|
13
|
-
SCAN_INTERVAL = 30 # seconds
|
14
7
|
MYHOSTNAME = Socket.gethostname
|
15
8
|
|
16
9
|
## remind me never to use inheritance again.
|
17
|
-
yaml_properties :uri, :
|
18
|
-
def initialize uri,
|
19
|
-
super uri,
|
10
|
+
yaml_properties :uri, :usual, :archived, :id, :labels
|
11
|
+
def initialize uri, usual=true, archived=false, id=nil, labels=[]
|
12
|
+
super uri, usual, archived, id
|
20
13
|
uri = URI(Source.expand_filesystem_uri(uri))
|
21
14
|
|
22
15
|
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
|
@@ -25,28 +18,14 @@ class Maildir < Source
|
|
25
18
|
|
26
19
|
@dir = uri.path
|
27
20
|
@labels = Set.new(labels || [])
|
28
|
-
@ids = []
|
29
|
-
@ids_to_fns = {}
|
30
|
-
@last_scan = nil
|
31
21
|
@mutex = Mutex.new
|
32
|
-
|
33
|
-
#these are used to determine whether scanning the directory for new mail
|
34
|
-
#is a worthwhile effort
|
35
|
-
@mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
|
36
|
-
@dir_ids = { 'cur' => [], 'new' => [] }
|
22
|
+
@mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
|
37
23
|
end
|
38
24
|
|
39
25
|
def file_path; @dir end
|
40
26
|
def self.suggest_labels_for path; [] end
|
41
27
|
def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
|
42
28
|
|
43
|
-
def check
|
44
|
-
scan_mailbox
|
45
|
-
return unless start_offset
|
46
|
-
|
47
|
-
start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
|
48
|
-
end
|
49
|
-
|
50
29
|
def store_message date, from_email, &block
|
51
30
|
stored = false
|
52
31
|
new_fn = new_maildir_basefn + ':2,S'
|
@@ -76,7 +55,6 @@ class Maildir < Source
|
|
76
55
|
end
|
77
56
|
|
78
57
|
def each_raw_message_line id
|
79
|
-
scan_mailbox
|
80
58
|
with_file_for(id) do |f|
|
81
59
|
until f.eof?
|
82
60
|
yield f.gets
|
@@ -85,17 +63,14 @@ class Maildir < Source
|
|
85
63
|
end
|
86
64
|
|
87
65
|
def load_header id
|
88
|
-
scan_mailbox
|
89
66
|
with_file_for(id) { |f| parse_raw_email_header f }
|
90
67
|
end
|
91
68
|
|
92
69
|
def load_message id
|
93
|
-
scan_mailbox
|
94
70
|
with_file_for(id) { |f| RMail::Parser.read f }
|
95
71
|
end
|
96
72
|
|
97
73
|
def raw_header id
|
98
|
-
scan_mailbox
|
99
74
|
ret = ""
|
100
75
|
with_file_for(id) do |f|
|
101
76
|
until f.eof? || (l = f.gets) =~ /^$/
|
@@ -106,106 +81,75 @@ class Maildir < Source
|
|
106
81
|
end
|
107
82
|
|
108
83
|
def raw_message id
|
109
|
-
scan_mailbox
|
110
84
|
with_file_for(id) { |f| f.read }
|
111
85
|
end
|
112
86
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
@mtimes
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@ids_to_fns[id] = fn
|
136
|
-
end
|
137
|
-
else
|
138
|
-
debug "no poll on #{d}. mtime on indicates no new messages."
|
139
|
-
end
|
87
|
+
## XXX use less memory
|
88
|
+
def poll
|
89
|
+
@mtimes.each do |d,prev_mtime|
|
90
|
+
subdir = File.join @dir, d
|
91
|
+
debug "polling maildir #{subdir}"
|
92
|
+
raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
|
93
|
+
mtime = File.mtime subdir
|
94
|
+
next if prev_mtime >= mtime
|
95
|
+
@mtimes[d] = mtime
|
96
|
+
|
97
|
+
old_ids = benchmark(:maildir_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id, "#{d}/").to_a }
|
98
|
+
new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.basename x }.sort }
|
99
|
+
added = new_ids - old_ids
|
100
|
+
deleted = old_ids - new_ids
|
101
|
+
debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
|
102
|
+
debug "#{added.size} added, #{deleted.size} deleted"
|
103
|
+
|
104
|
+
added.each_with_index do |id,i|
|
105
|
+
yield :add,
|
106
|
+
:info => File.join(d,id),
|
107
|
+
:labels => @labels + maildir_labels(id) + [:inbox],
|
108
|
+
:progress => i.to_f/(added.size+deleted.size)
|
140
109
|
end
|
141
|
-
@ids = @dir_ids.values.flatten.uniq.sort!
|
142
|
-
rescue SystemCallError, IOError => e
|
143
|
-
raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
|
144
|
-
end
|
145
|
-
|
146
|
-
debug "done scanning maildir"
|
147
|
-
@last_scan = Time.now
|
148
|
-
end
|
149
|
-
synchronized :scan_mailbox
|
150
110
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
start.upto(@ids.length - 1) do |i|
|
158
|
-
id = @ids[i]
|
159
|
-
self.cur_offset = id
|
160
|
-
yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
|
111
|
+
deleted.each_with_index do |id,i|
|
112
|
+
yield :delete,
|
113
|
+
:info => File.join(d,id),
|
114
|
+
:progress => (i.to_f+added.size)/(added.size+deleted.size)
|
115
|
+
end
|
161
116
|
end
|
117
|
+
nil
|
162
118
|
end
|
163
119
|
|
164
|
-
def
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
def end_offset
|
170
|
-
scan_mailbox :rescan => true
|
171
|
-
@ids.last + 1
|
120
|
+
def maildir_labels id
|
121
|
+
(seen?(id) ? [] : [:unread]) +
|
122
|
+
(trashed?(id) ? [:deleted] : []) +
|
123
|
+
(flagged?(id) ? [:starred] : [])
|
172
124
|
end
|
173
125
|
|
174
|
-
def
|
126
|
+
def draft? id; maildir_data(id)[2].include? "D"; end
|
127
|
+
def flagged? id; maildir_data(id)[2].include? "F"; end
|
128
|
+
def passed? id; maildir_data(id)[2].include? "P"; end
|
129
|
+
def replied? id; maildir_data(id)[2].include? "R"; end
|
130
|
+
def seen? id; maildir_data(id)[2].include? "S"; end
|
131
|
+
def trashed? id; maildir_data(id)[2].include? "T"; end
|
175
132
|
|
176
|
-
def
|
177
|
-
def
|
178
|
-
def
|
179
|
-
def
|
180
|
-
def
|
181
|
-
def
|
133
|
+
def mark_draft id; maildir_mark_file id, "D" unless draft? id; end
|
134
|
+
def mark_flagged id; maildir_mark_file id, "F" unless flagged? id; end
|
135
|
+
def mark_passed id; maildir_mark_file id, "P" unless passed? id; end
|
136
|
+
def mark_replied id; maildir_mark_file id, "R" unless replied? id; end
|
137
|
+
def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
|
138
|
+
def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
|
182
139
|
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
|
187
|
-
def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
|
188
|
-
def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
|
189
|
-
|
190
|
-
def filename_for_id id; @ids_to_fns[id] end
|
140
|
+
def valid? id
|
141
|
+
File.exists? File.join(@dir, id)
|
142
|
+
end
|
191
143
|
|
192
144
|
private
|
193
145
|
|
194
|
-
def make_id fn
|
195
|
-
#doing this means 1 syscall instead of 2 (File.mtime, File.size).
|
196
|
-
#makes a noticeable difference on nfs.
|
197
|
-
stat = File.stat(fn)
|
198
|
-
# use 7 digits for the size. why 7? seems nice.
|
199
|
-
sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
|
200
|
-
end
|
201
|
-
|
202
146
|
def new_maildir_basefn
|
203
147
|
Kernel::srand()
|
204
148
|
"#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
|
205
149
|
end
|
206
150
|
|
207
151
|
def with_file_for id
|
208
|
-
fn = @
|
152
|
+
fn = File.join(@dir, id)
|
209
153
|
begin
|
210
154
|
File.open(fn, 'rb') { |f| yield f }
|
211
155
|
rescue SystemCallError, IOError => e
|
@@ -213,10 +157,9 @@ private
|
|
213
157
|
end
|
214
158
|
end
|
215
159
|
|
216
|
-
def maildir_data
|
217
|
-
|
218
|
-
|
219
|
-
[($1 || fn), ($2 || "2"), ($3 || "")]
|
160
|
+
def maildir_data id
|
161
|
+
id =~ %r{^([^:]+):([12]),([DFPRST]*)$}
|
162
|
+
[($1 || id), ($2 || "2"), ($3 || "")]
|
220
163
|
end
|
221
164
|
|
222
165
|
## not thread-safe on msg
|
data/lib/sup/mbox.rb
CHANGED
@@ -1,13 +1,155 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require "sup/mbox/ssh-loader"
|
1
|
+
require 'uri'
|
2
|
+
require 'set'
|
4
3
|
|
5
4
|
module Redwood
|
6
5
|
|
7
|
-
|
6
|
+
class MBox < Source
|
8
7
|
BREAK_RE = /^From \S+ (.+)$/
|
9
8
|
|
10
|
-
|
9
|
+
include SerializeLabelsNicely
|
10
|
+
yaml_properties :uri, :usual, :archived, :id, :labels
|
11
|
+
|
12
|
+
attr_reader :labels
|
13
|
+
|
14
|
+
## uri_or_fp is horrific. need to refactor.
|
15
|
+
def initialize uri_or_fp, usual=true, archived=false, id=nil, labels=nil
|
16
|
+
@mutex = Mutex.new
|
17
|
+
@labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
|
18
|
+
|
19
|
+
case uri_or_fp
|
20
|
+
when String
|
21
|
+
uri = URI(Source.expand_filesystem_uri(uri_or_fp))
|
22
|
+
raise ArgumentError, "not an mbox uri" unless uri.scheme == "mbox"
|
23
|
+
raise ArgumentError, "mbox URI ('#{uri}') cannot have a host: #{uri.host}" if uri.host
|
24
|
+
raise ArgumentError, "mbox URI must have a path component" unless uri.path
|
25
|
+
@f = File.open uri.path, 'rb'
|
26
|
+
@path = uri.path
|
27
|
+
else
|
28
|
+
@f = uri_or_fp
|
29
|
+
@path = uri_or_fp.path
|
30
|
+
end
|
31
|
+
|
32
|
+
super uri_or_fp, usual, archived, id
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_path; @path end
|
36
|
+
def is_source_for? uri; super || (self.uri.is_a?(String) && (URI(Source.expand_filesystem_uri(uri)) == URI(Source.expand_filesystem_uri(self.uri)))) end
|
37
|
+
|
38
|
+
def self.suggest_labels_for path
|
39
|
+
## heuristic: use the filename as a label, unless the file
|
40
|
+
## has a path that probably represents an inbox.
|
41
|
+
if File.dirname(path) =~ /\b(var|usr|spool)\b/
|
42
|
+
[]
|
43
|
+
else
|
44
|
+
[File.basename(path).downcase.intern]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def load_header offset
|
49
|
+
header = nil
|
50
|
+
@mutex.synchronize do
|
51
|
+
@f.seek offset
|
52
|
+
header = parse_raw_email_header @f
|
53
|
+
end
|
54
|
+
header
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_message offset
|
58
|
+
@mutex.synchronize do
|
59
|
+
@f.seek offset
|
60
|
+
begin
|
61
|
+
## don't use RMail::Mailbox::MBoxReader because it doesn't properly ignore
|
62
|
+
## "From" at the start of a message body line.
|
63
|
+
string = ""
|
64
|
+
until @f.eof? || MBox::is_break_line?(l = @f.gets)
|
65
|
+
string << l
|
66
|
+
end
|
67
|
+
RMail::Parser.read string
|
68
|
+
rescue RMail::Parser::Error => e
|
69
|
+
raise FatalSourceError, "error parsing mbox file: #{e.message}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def raw_header offset
|
75
|
+
ret = ""
|
76
|
+
@mutex.synchronize do
|
77
|
+
@f.seek offset
|
78
|
+
until @f.eof? || (l = @f.gets) =~ /^\r*$/
|
79
|
+
ret << l
|
80
|
+
end
|
81
|
+
end
|
82
|
+
ret
|
83
|
+
end
|
84
|
+
|
85
|
+
def raw_message offset
|
86
|
+
ret = ""
|
87
|
+
each_raw_message_line(offset) { |l| ret << l }
|
88
|
+
ret
|
89
|
+
end
|
90
|
+
|
91
|
+
def store_message date, from_email, &block
|
92
|
+
need_blank = File.exists?(@path) && !File.zero?(@path)
|
93
|
+
File.open(@path, "ab") do |f|
|
94
|
+
f.puts if need_blank
|
95
|
+
f.puts "From #{from_email} #{date.asctime}"
|
96
|
+
yield f
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
## apparently it's a million times faster to call this directly if
|
101
|
+
## we're just moving messages around on disk, than reading things
|
102
|
+
## into memory with raw_message.
|
103
|
+
##
|
104
|
+
## i hoped never to have to move shit around on disk but
|
105
|
+
## sup-sync-back has to do it.
|
106
|
+
def each_raw_message_line offset
|
107
|
+
@mutex.synchronize do
|
108
|
+
@f.seek offset
|
109
|
+
until @f.eof? || MBox::is_break_line?(l = @f.gets)
|
110
|
+
yield l
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def default_labels
|
116
|
+
[:inbox, :unread]
|
117
|
+
end
|
118
|
+
|
119
|
+
def poll
|
120
|
+
first_offset = first_new_message
|
121
|
+
offset = first_offset
|
122
|
+
end_offset = File.size @f
|
123
|
+
while offset and offset < end_offset
|
124
|
+
yield :add,
|
125
|
+
:info => offset,
|
126
|
+
:labels => (labels + default_labels),
|
127
|
+
:progress => (offset - first_offset).to_f/end_offset
|
128
|
+
offset = next_offset offset
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def next_offset offset
|
133
|
+
@mutex.synchronize do
|
134
|
+
@f.seek offset
|
135
|
+
nil while line = @f.gets and not MBox::is_break_line? line
|
136
|
+
offset = @f.tell
|
137
|
+
offset != File.size(@f) ? offset : nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
## TODO optimize this by iterating over allterms list backwards or
|
142
|
+
## storing source_info negated
|
143
|
+
def last_indexed_message
|
144
|
+
benchmark(:mbox_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id).map(&:to_i).max }
|
145
|
+
end
|
146
|
+
|
147
|
+
## offset of first new message or nil
|
148
|
+
def first_new_message
|
149
|
+
next_offset(last_indexed_message || 0)
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.is_break_line? l
|
11
153
|
l =~ BREAK_RE or return false
|
12
154
|
time = $1
|
13
155
|
begin
|
@@ -19,6 +161,9 @@ module MBox
|
|
19
161
|
false
|
20
162
|
end
|
21
163
|
end
|
22
|
-
|
164
|
+
|
165
|
+
class Loader < self
|
166
|
+
yaml_properties :uri, :usual, :archived, :id, :labels
|
167
|
+
end
|
23
168
|
end
|
24
169
|
end
|
data/lib/sup/message-chunks.rb
CHANGED
@@ -32,9 +32,25 @@ require 'tempfile'
|
|
32
32
|
## attachments are quotable; Signatures are not.
|
33
33
|
|
34
34
|
## monkey-patch time: make temp files have the right extension
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
## Backport from Ruby 1.9.2 for versions lower than 1.8.7
|
36
|
+
if RUBY_VERSION < '1.8.7'
|
37
|
+
class Tempfile
|
38
|
+
def make_tmpname(prefix_suffix, n)
|
39
|
+
case prefix_suffix
|
40
|
+
when String
|
41
|
+
prefix = prefix_suffix
|
42
|
+
suffix = ""
|
43
|
+
when Array
|
44
|
+
prefix = prefix_suffix[0]
|
45
|
+
suffix = prefix_suffix[1]
|
46
|
+
else
|
47
|
+
raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
|
48
|
+
end
|
49
|
+
t = Time.now.strftime("%Y%m%d")
|
50
|
+
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
|
51
|
+
path << "-#{n}" if n
|
52
|
+
path << suffix
|
53
|
+
end
|
38
54
|
end
|
39
55
|
end
|
40
56
|
|
@@ -149,7 +165,7 @@ EOS
|
|
149
165
|
end
|
150
166
|
|
151
167
|
def write_to_disk
|
152
|
-
file = Tempfile.new(@filename || "sup-attachment")
|
168
|
+
file = Tempfile.new(["sup", @filename.gsub("/", "_") || "sup-attachment"])
|
153
169
|
file.print @raw_content
|
154
170
|
file.close
|
155
171
|
file.path
|