rgrove-larch 1.0.0.8 → 1.0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/larch/imap/mailbox.rb +276 -0
  2. 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.8
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