rgrove-larch 1.0.0.8 → 1.0.0.9
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.
- data/lib/larch/imap/mailbox.rb +276 -0
- metadata +2 -1
@@ -0,0 +1,276 @@
|
|
1
|
+
module Larch; class IMAP
|
2
|
+
|
3
|
+
# Represents an IMAP mailbox.
|
4
|
+
class Mailbox
|
5
|
+
attr_reader :attr, :delim, :imap, :name, :state
|
6
|
+
|
7
|
+
# Maximum number of messages to fetch at once.
|
8
|
+
MAX_FETCH_COUNT = 1024
|
9
|
+
|
10
|
+
# Regex to capture a Message-Id header.
|
11
|
+
REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
|
12
|
+
|
13
|
+
# Minimum time (in seconds) allowed between mailbox scans.
|
14
|
+
SCAN_INTERVAL = 60
|
15
|
+
|
16
|
+
def initialize(imap, name, delim, *attr)
|
17
|
+
raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
|
18
|
+
|
19
|
+
@delim = delim
|
20
|
+
@imap = imap
|
21
|
+
@name = name
|
22
|
+
@attr = attr
|
23
|
+
|
24
|
+
@ids = {}
|
25
|
+
@last_id = 0
|
26
|
+
@last_scan = nil
|
27
|
+
@mutex = Mutex.new
|
28
|
+
|
29
|
+
# Valid mailbox states are :closed (no mailbox open), :examined (mailbox
|
30
|
+
# open and read-only), or :selected (mailbox open and read-write).
|
31
|
+
@state = :closed
|
32
|
+
|
33
|
+
# Create private convenience methods (debug, info, warn, etc.) to make
|
34
|
+
# logging easier.
|
35
|
+
Logger::LEVELS.each_key do |level|
|
36
|
+
Mailbox.class_eval do
|
37
|
+
define_method(level) do |msg|
|
38
|
+
Larch.log.log(level, "#{@imap.username}@#{@imap.host}: #{@name}: #{msg}")
|
39
|
+
end
|
40
|
+
|
41
|
+
private level
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
|
47
|
+
# already exist. Returns +true+ if the message was appended successfully,
|
48
|
+
# +false+ if the message already exists in the mailbox.
|
49
|
+
def append(message)
|
50
|
+
raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Larch::IMAP::Message)
|
51
|
+
return false if has_message?(message)
|
52
|
+
|
53
|
+
@imap.safely do
|
54
|
+
imap_select(!!@imap.options[:create_mailbox])
|
55
|
+
|
56
|
+
debug "appending message: #{message.id}"
|
57
|
+
@imap.conn.append(@name, message.rfc822, message.flags, message.internaldate)
|
58
|
+
end
|
59
|
+
|
60
|
+
true
|
61
|
+
end
|
62
|
+
alias << append
|
63
|
+
|
64
|
+
# Iterates through Larch message ids in this mailbox, yielding each one to the
|
65
|
+
# provided block.
|
66
|
+
def each
|
67
|
+
scan
|
68
|
+
@ids.dup.each_key {|id| yield id }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Gets a Net::IMAP::Envelope for the specified message id.
|
72
|
+
def envelope(message_id)
|
73
|
+
scan
|
74
|
+
raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
|
75
|
+
|
76
|
+
debug "fetching envelope: #{message_id}"
|
77
|
+
imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
78
|
+
end
|
79
|
+
|
80
|
+
# Fetches a Larch::IMAP::Message struct representing the message with the
|
81
|
+
# specified Larch message id.
|
82
|
+
def fetch(message_id, peek = false)
|
83
|
+
scan
|
84
|
+
raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
|
85
|
+
|
86
|
+
debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
|
87
|
+
data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
|
88
|
+
|
89
|
+
Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
|
90
|
+
data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
|
91
|
+
end
|
92
|
+
alias [] fetch
|
93
|
+
|
94
|
+
# Returns +true+ if a message with the specified Larch <em>message_id</em>
|
95
|
+
# exists in this mailbox, +false+ otherwise.
|
96
|
+
def has_message?(message_id)
|
97
|
+
scan
|
98
|
+
@ids.has_key?(message_id)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Gets the number of messages in this mailbox.
|
102
|
+
def length
|
103
|
+
scan
|
104
|
+
@ids.length
|
105
|
+
end
|
106
|
+
alias size length
|
107
|
+
|
108
|
+
# Same as fetch, but doesn't mark the message as seen.
|
109
|
+
def peek(message_id)
|
110
|
+
fetch(message_id, true)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Resets the mailbox state.
|
114
|
+
def reset
|
115
|
+
@mutex.synchronize { @state = :closed }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Fetches message headers from this mailbox.
|
119
|
+
def scan
|
120
|
+
return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
|
121
|
+
|
122
|
+
begin
|
123
|
+
imap_examine
|
124
|
+
rescue Error => e
|
125
|
+
return if @imap.options[:create_mailbox]
|
126
|
+
raise
|
127
|
+
end
|
128
|
+
|
129
|
+
last_id = @imap.safely { @imap.conn.responses['EXISTS'].last }
|
130
|
+
|
131
|
+
@mutex.synchronize { @last_scan = Time.now }
|
132
|
+
return if last_id == @last_id
|
133
|
+
|
134
|
+
range = (@last_id + 1)..last_id
|
135
|
+
|
136
|
+
@mutex.synchronize { @last_id = last_id }
|
137
|
+
|
138
|
+
info "fetching message headers #{range}" <<
|
139
|
+
(@imap.options[:fast_scan] ? ' (fast scan)' : '')
|
140
|
+
|
141
|
+
fields = if @imap.options[:fast_scan]
|
142
|
+
['UID', 'RFC822.SIZE', 'INTERNALDATE']
|
143
|
+
else
|
144
|
+
"(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
|
145
|
+
end
|
146
|
+
|
147
|
+
imap_fetch(range, fields).each do |data|
|
148
|
+
id = create_id(data)
|
149
|
+
|
150
|
+
unless uid = data.attr['UID']
|
151
|
+
error "UID not in IMAP response for message: #{id}"
|
152
|
+
next
|
153
|
+
end
|
154
|
+
|
155
|
+
if Larch.log.level == :debug && @ids.has_key?(id)
|
156
|
+
envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
157
|
+
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
158
|
+
end
|
159
|
+
|
160
|
+
@mutex.synchronize { @ids[id] = uid }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Creates an id suitable for uniquely identifying a specific message across
|
167
|
+
# servers (we hope).
|
168
|
+
#
|
169
|
+
# If the given message data includes a valid Message-Id header, then that will
|
170
|
+
# be used to generate an MD5 hash. Otherwise, the hash will be generated based
|
171
|
+
# on the message's RFC822.SIZE and INTERNALDATE.
|
172
|
+
def create_id(data)
|
173
|
+
['RFC822.SIZE', 'INTERNALDATE'].each do |a|
|
174
|
+
raise Error, "required data not in IMAP response: #{a}" unless data.attr[a]
|
175
|
+
end
|
176
|
+
|
177
|
+
if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
|
178
|
+
Digest::MD5.hexdigest($1)
|
179
|
+
else
|
180
|
+
Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
|
181
|
+
Time.parse(data.attr['INTERNALDATE']).to_i))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Examines the mailbox. If _force_ is true, the mailbox will be examined even
|
186
|
+
# if it is already selected (which isn't necessary unless you want to ensure
|
187
|
+
# that it's in a read-only state).
|
188
|
+
def imap_examine(force = false)
|
189
|
+
return if @state == :examined || (!force && @state == :selected)
|
190
|
+
|
191
|
+
@imap.safely do
|
192
|
+
begin
|
193
|
+
@mutex.synchronize { @state = :closed }
|
194
|
+
|
195
|
+
debug "examining mailbox"
|
196
|
+
@imap.conn.examine(@name)
|
197
|
+
|
198
|
+
@mutex.synchronize { @state = :examined }
|
199
|
+
|
200
|
+
rescue Net::IMAP::NoResponseError => e
|
201
|
+
raise Error, "unable to examine mailbox: #{e.message}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Fetches the specified _fields_ for the specified message sequence id(s) from
|
207
|
+
# this mailbox.
|
208
|
+
def imap_fetch(ids, fields)
|
209
|
+
ids = ids.to_a
|
210
|
+
data = []
|
211
|
+
pos = 0
|
212
|
+
|
213
|
+
while pos < ids.length
|
214
|
+
@imap.safely do
|
215
|
+
imap_examine
|
216
|
+
|
217
|
+
data += @imap.conn.fetch(ids[pos, MAX_FETCH_COUNT], fields)
|
218
|
+
pos += MAX_FETCH_COUNT
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
data
|
223
|
+
end
|
224
|
+
|
225
|
+
# Selects the mailbox if it is not already selected. If the mailbox does not
|
226
|
+
# exist and _create_ is +true+, it will be created. Otherwise, a
|
227
|
+
# Larch::IMAP::Error will be raised.
|
228
|
+
def imap_select(create = false)
|
229
|
+
return if @state == :selected
|
230
|
+
|
231
|
+
@imap.safely do
|
232
|
+
begin
|
233
|
+
@mutex.synchronize { @state = :closed }
|
234
|
+
|
235
|
+
debug "selecting mailbox"
|
236
|
+
@imap.conn.select(@name)
|
237
|
+
|
238
|
+
@mutex.synchronize { @state = :selected }
|
239
|
+
|
240
|
+
rescue Net::IMAP::NoResponseError => e
|
241
|
+
raise Error, "unable to select mailbox: #{e.message}" unless create
|
242
|
+
|
243
|
+
info "creating mailbox: #{mailbox}"
|
244
|
+
|
245
|
+
begin
|
246
|
+
@imap.conn.create(@name)
|
247
|
+
retry
|
248
|
+
rescue => e
|
249
|
+
raise Error, "unable to create mailbox: #{e.message}"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Fetches the specified _fields_ for the specified UID(s) from the IMAP
|
256
|
+
# server.
|
257
|
+
def imap_uid_fetch(uids, fields)
|
258
|
+
uids = uids.to_a
|
259
|
+
data = []
|
260
|
+
pos = 0
|
261
|
+
|
262
|
+
while pos < uids.length
|
263
|
+
@imap.safely do
|
264
|
+
imap_examine
|
265
|
+
|
266
|
+
data += @imap.conn.uid_fetch(uids[pos, MAX_FETCH_COUNT], fields)
|
267
|
+
pos += MAX_FETCH_COUNT
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
data
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
end; end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rgrove-larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Grove
|
@@ -48,6 +48,7 @@ files:
|
|
48
48
|
- lib/larch.rb
|
49
49
|
- lib/larch/errors.rb
|
50
50
|
- lib/larch/imap.rb
|
51
|
+
- lib/larch/imap/mailbox.rb
|
51
52
|
- lib/larch/logger.rb
|
52
53
|
- lib/larch/version.rb
|
53
54
|
has_rdoc: false
|